Files
zstd-rs/rust/src/zstd_compress.rs
T
ddidderr 48eb895af3 feat(compress): move raw and RLE block emitters to Rust
Replace the shared C raw and one-byte RLE fallback block serializers with Rust ABI leaves. Preserve the zstd block headers, payload copies, capacity errors, and all existing C compressor dispatch and context ownership; share the raw serializer with the Rust one-shot path.

Test Plan:

- cargo test --manifest-path rust/Cargo.toml --no-default-features --features compression (208 tests)

- cargo clippy --manifest-path rust/Cargo.toml

- cargo clippy --manifest-path rust/Cargo.toml --benches

- cargo clippy --manifest-path rust/Cargo.toml --tests

- make -B -C lib -j2 lib
2026-07-18 06:03:39 +02:00

805 lines
27 KiB
Rust

#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::too_many_arguments)]
//! First high-level compression slice.
//!
//! The layout-independent one-shot entry point in this module drives the
//! already migrated compression leaves. `ZSTD_compressCCtx` uses the same
//! path after a narrow C-owned context reset; the public context lifecycle
//! remains C-owned because it shares the configuration-dependent private
//! `ZSTD_CCtx_s` layout. `ZSTD_compress2` and the complete-input simple
//! `ZSTD_compressStream2(..., ZSTD_e_end)` path dispatch through Rust while
//! retaining the C implementation for advanced and partial-stream cases.
use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
#[cfg(not(test))]
use crate::zstd_compress_api::ZSTD_compressBound;
use crate::zstd_compress_frame::{
write_raw_block, ZSTD_rust_writeFrameHeader, ZSTD_writeLastEmptyBlock,
};
use crate::zstd_compress_params::{
ZSTD_rust_params_adjustCParams, ZSTD_rust_params_maxNbSeq, ZSTD_rust_params_selectCParams,
ZSTD_RUST_CPM_NO_ATTACH_DICT, ZSTD_RUST_PS_DISABLE,
};
use crate::zstd_compress_sequences::SeqDef;
use crate::zstd_compress_stats::{SeqStore_t, ZSTD_compressedBlockState_t};
use crate::zstd_compress_superblock::ZSTD_rust_compressSuperBlock;
use std::ffi::c_void;
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;
fn ZSTD_rust_prepareCCtxForSimpleCompression(
cctx: *mut c_void,
src_size: usize,
compression_level: c_int,
) -> usize;
fn ZSTD_rust_resetCCtxForSimpleCompressionSession(cctx: *mut c_void) -> usize;
fn ZSTD_rust_markSimpleCompression2Complete(cctx: *mut c_void);
fn ZSTD_rust_simpleCompress2Level(cctx: *const c_void) -> c_int;
fn ZSTD_rust_simpleCompressStream2Level(cctx: *const c_void) -> c_int;
fn ZSTD_compress2_c(
cctx: *mut c_void,
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
) -> usize;
fn ZSTD_compressStream2_c(
cctx: *mut c_void,
output: *mut ZSTD_outBuffer,
input: *mut ZSTD_inBuffer,
end_op: c_int,
) -> 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;
const ZSTD_ROWSIZE: usize = 16;
const ZSTD_WINDOW_START_INDEX: u32 = 2;
const ZSTD_DUBT_UNSORTED_MARK: u32 = 1;
const ZSTD_INDEXOVERFLOW_MARGIN: usize = 16usize << 20;
const ZSTD_CURRENT_MAX: usize = if size_of::<usize>() == 8 {
3500usize << 20
} else {
2000usize << 20
};
const ZSTD_CHUNKSIZE_MAX: usize = u32::MAX as usize - ZSTD_CURRENT_MAX;
#[cfg(not(test))]
const ZSTD_E_END: c_int = 2;
#[cfg(not(test))]
#[repr(C)]
pub struct ZSTD_inBuffer {
src: *const c_void,
size: usize,
pos: usize,
}
#[cfg(not(test))]
#[repr(C)]
pub struct ZSTD_outBuffer {
dst: *mut c_void,
size: usize,
pos: usize,
}
/* HUF_WORKSPACE_SIZE + (MaxSeq + 2) * sizeof(unsigned), rounded up. The
* superblock leaf also accepts the larger pre-split workspace, so a fixed
* 16 KiB buffer is sufficient for this first non-splitting path on both
* supported pointer widths. */
const TMP_WORKSPACE_SIZE: usize = 16 << 10;
#[inline]
fn bitmix(mut val: u64, len: u64) -> u64 {
val ^= val.rotate_right(49) ^ val.rotate_right(24);
val = val.wrapping_mul(0x9FB21C651E98DF25);
val ^= (val >> 35).wrapping_add(len);
val = val.wrapping_mul(0x9FB21C651E98DF25);
val ^ (val >> 28)
}
#[inline]
fn advance_hash_salt(hash_salt: u64, hash_salt_entropy: u64) -> u64 {
bitmix(hash_salt, 8) ^ bitmix(hash_salt_entropy, 4)
}
/// Advance the row-matchfinder salt without exposing C's private match state.
#[no_mangle]
pub extern "C" fn ZSTD_rust_advanceHashSalt(hash_salt: u64, hash_salt_entropy: u64) -> u64 {
advance_hash_salt(hash_salt, hash_salt_entropy)
}
#[inline]
fn index_too_close_to_max(next_src_base_offset: usize) -> bool {
next_src_base_offset > ZSTD_CURRENT_MAX - ZSTD_INDEXOVERFLOW_MARGIN
}
/// Return whether a scalar C window offset is within the overflow margin.
#[no_mangle]
pub extern "C" fn ZSTD_rust_indexTooCloseToMax(next_src_base_offset: usize) -> c_int {
index_too_close_to_max(next_src_base_offset) as c_int
}
#[inline]
fn dict_too_big(loaded_dict_size: usize) -> bool {
loaded_dict_size > ZSTD_CHUNKSIZE_MAX
}
/// Return whether a dictionary exceeds the maximum loadable chunk size.
#[no_mangle]
pub extern "C" fn ZSTD_rust_dictTooBig(loaded_dict_size: usize) -> c_int {
dict_too_big(loaded_dict_size) as c_int
}
#[inline]
fn reduce_table_internal(table: &mut [u32], reducer_value: u32, preserve_mark: bool) {
debug_assert_eq!(table.len() % ZSTD_ROWSIZE, 0);
debug_assert!(table.len() < (1usize << 31));
/* Protect special index values < ZSTD_WINDOW_START_INDEX. */
let reducer_threshold = reducer_value.wrapping_add(ZSTD_WINDOW_START_INDEX);
let mut rows = table.chunks_exact_mut(ZSTD_ROWSIZE);
for row in &mut rows {
for cell in row {
let value = *cell;
*cell = if preserve_mark && value == ZSTD_DUBT_UNSORTED_MARK {
/* Keep the btlazy2 unsorted marker across table reduction. */
ZSTD_DUBT_UNSORTED_MARK
} else if value < reducer_threshold {
0
} else {
value.wrapping_sub(reducer_value)
};
}
}
debug_assert!(rows.into_remainder().is_empty());
}
/// Rust implementation of the C match-table reduction leaf.
///
/// The C wrappers select the ordinary or btlazy2 policy by passing a clear
/// zero/one `preserve_mark` value; `ZSTD_reduceIndex` remains C-owned.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_reduceTable(
table: *mut u32,
size: u32,
reducer_value: u32,
preserve_mark: c_int,
) {
debug_assert!(!table.is_null() || size == 0);
debug_assert_eq!(size % ZSTD_ROWSIZE as u32, 0);
debug_assert!(size < (1u32 << 31));
if size == 0 {
return;
}
let table = unsafe { std::slice::from_raw_parts_mut(table, size as usize) };
reduce_table_internal(table, reducer_value, preserve_mark != 0);
}
#[inline]
fn zeroed_state() -> ZSTD_compressedBlockState_t {
/* The state contains only integer arrays and enum fields. */
unsafe { MaybeUninit::<ZSTD_compressedBlockState_t>::zeroed().assume_init() }
}
#[inline]
fn checked_table_size(log: u32) -> Option<usize> {
1usize.checked_shl(log)
}
#[inline]
fn ceil_log2(size: usize) -> u32 {
if size <= 1 {
0
} else {
usize::BITS - (size - 1).leading_zeros()
}
}
/// Compress one frame using the already migrated block leaves.
///
/// This path deliberately starts a fresh match table for each 128 KiB block.
/// That keeps the Rust-owned context independent from the still-private C
/// `ZSTD_MatchState_t` window while producing ordinary zstd blocks that any
/// decoder can consume.
unsafe fn compress_frame(
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
compression_level: c_int,
) -> usize {
if dst.is_null() {
return ERROR(if dst_capacity == 0 {
ZstdErrorCode::DstSizeTooSmall
} else {
ZstdErrorCode::DstBufferNull
});
}
if src_size != 0 && src.is_null() {
return ERROR(ZstdErrorCode::SrcSizeWrong);
}
if src_size as u64 == ZSTD_CONTENTSIZE_UNKNOWN {
return ERROR(ZstdErrorCode::SrcSizeWrong);
}
let mut cparams = ZSTD_rust_params_selectCParams(
compression_level,
src_size as u64,
0,
ZSTD_RUST_CPM_NO_ATTACH_DICT,
);
cparams = ZSTD_rust_params_adjustCParams(
cparams,
src_size as u64,
0,
ZSTD_RUST_CPM_NO_ATTACH_DICT,
ZSTD_RUST_PS_DISABLE,
);
let header_size = unsafe {
ZSTD_rust_writeFrameHeader(
dst,
dst_capacity,
0, /* noDictIDFlag */
0, /* checksumFlag */
1, /* contentSizeFlag */
0, /* zstd frame */
cparams.windowLog,
src_size as u64,
0,
)
};
if ERR_isError(header_size) {
return header_size;
}
if src_size == 0 {
let empty_block = unsafe {
ZSTD_writeLastEmptyBlock(
dst.cast::<u8>().add(header_size).cast(),
dst_capacity - header_size,
)
};
return if ERR_isError(empty_block) {
empty_block
} else {
header_size + empty_block
};
}
let first_block_size = src_size.min(ZSTD_BLOCKSIZE_MAX);
/* A table larger than the block cannot contain a useful index for this
* independent-block path. This also keeps one-shot small inputs from
* allocating the table selected for a huge source-size hint. */
let matcher_hash_log = cparams.hashLog.min(ceil_log2(first_block_size).max(6));
let matcher_chain_log = cparams.chainLog.min(ceil_log2(first_block_size).max(6));
let hash_size = match checked_table_size(matcher_hash_log) {
Some(size) => size,
None => return ERROR(ZstdErrorCode::MemoryAllocation),
};
let chain_size = match checked_table_size(matcher_chain_log) {
Some(size) => size,
None => return ERROR(ZstdErrorCode::MemoryAllocation),
};
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];
let mut ml_codes = vec![0u8; max_nb_seq];
let mut of_codes = vec![0u8; max_nb_seq];
let mut hash_table = vec![0u32; hash_size];
let mut chain_table = vec![0u32; chain_size];
let mut workspace = vec![0u64; TMP_WORKSPACE_SIZE / size_of::<u64>()];
let mut prev_block = zeroed_state();
let mut next_block = zeroed_state();
prev_block.rep = [1, 4, 8];
next_block.rep = prev_block.rep;
let seq_store = &mut SeqStore_t {
sequencesStart: sequences.as_mut_ptr(),
sequences: sequences.as_mut_ptr(),
litStart: literals.as_mut_ptr(),
lit: literals.as_mut_ptr(),
llCode: ll_codes.as_mut_ptr(),
mlCode: ml_codes.as_mut_ptr(),
ofCode: of_codes.as_mut_ptr(),
maxNbSeq: max_nb_seq,
maxNbLit: ZSTD_BLOCKSIZE_MAX,
longLengthType: 0,
longLengthPos: 0,
};
let source = src.cast::<u8>();
let output = dst.cast::<u8>();
let mut input_offset = 0usize;
let mut output_offset = header_size;
while input_offset < src_size {
let block_size = (src_size - input_offset).min(ZSTD_BLOCKSIZE_MAX);
let block_src = unsafe { source.add(input_offset) };
let block_end = unsafe { block_src.add(block_size) };
seq_store.sequences = seq_store.sequencesStart;
seq_store.lit = seq_store.litStart;
seq_store.longLengthType = 0;
seq_store.longLengthPos = 0;
next_block.rep = prev_block.rep;
let mut reps = prev_block.rep;
let last_literals = if block_size < 8 {
block_size
} else if cparams.strategy == ZSTD_DFAST {
unsafe {
crate::zstd_double_fast::ZSTD_rust_compressBlock_doubleFast(
hash_table.as_mut_ptr(),
chain_table.as_mut_ptr(),
block_src,
0,
0,
matcher_hash_log,
matcher_chain_log,
cparams.minMatch,
cparams.windowLog,
(seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
block_src.cast(),
block_size,
)
}
} else {
unsafe {
crate::zstd_fast::ZSTD_rust_compressBlock_fast(
hash_table.as_mut_ptr(),
block_src,
0,
0,
matcher_hash_log,
cparams.minMatch,
cparams.targetLength,
cparams.windowLog,
(seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
block_src.cast(),
block_size,
)
}
};
if last_literals > block_size {
return ERROR(ZstdErrorCode::Generic);
}
let last_literal_src = unsafe { block_end.sub(last_literals) };
if last_literals != 0 {
unsafe {
ptr::copy_nonoverlapping(last_literal_src, seq_store.lit, last_literals);
seq_store.lit = seq_store.lit.add(last_literals);
}
}
next_block.rep = reps;
let last_block = u32::from(input_offset + block_size == src_size);
let remaining_capacity = dst_capacity.saturating_sub(output_offset);
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,
disable_literal_compression,
workspace.as_mut_ptr().cast(),
TMP_WORKSPACE_SIZE,
0, /* BMI2 is optional; portable Rust leaf path */
cparams.windowLog,
0, /* no target compressed block size */
output.add(output_offset).cast(),
remaining_capacity,
block_src.cast(),
block_size,
last_block,
)
};
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);
}
output_offset += written;
input_offset += block_size;
std::mem::swap(&mut prev_block, &mut next_block);
hash_table.fill(0);
chain_table.fill(0);
}
output_offset
}
/// Simple one-shot compression entry point.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_compress(
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
compression_level: c_int,
) -> usize {
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;
}
let prepare =
unsafe { ZSTD_rust_prepareCCtxForSimpleCompression(cctx, src_size, compression_level) };
if ERR_isError(prepare) {
return prepare;
}
}
unsafe { compress_frame(dst, dst_capacity, src, src_size, compression_level) }
}
/// Stateful compression entry point during the context migration.
///
/// A context with only the ordinary frame settings is reset by the C shim and
/// compressed through the Rust frame path. Contexts using dictionaries,
/// checksums, target-sized blocks, sequence collection, or other advanced
/// state still use the renamed C implementation until their state is moved.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_compress2(
cctx: *mut c_void,
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
) -> usize {
if cctx.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
#[cfg(test)]
{
unsafe { compress_frame(dst, dst_capacity, src, src_size, 3) }
}
#[cfg(not(test))]
{
let level = unsafe { ZSTD_rust_simpleCompress2Level(cctx.cast_const()) };
if level != c_int::MIN {
let reset = unsafe { ZSTD_rust_resetCCtxForSimpleCompressionSession(cctx) };
if ERR_isError(reset) {
return reset;
}
let prepare =
unsafe { ZSTD_rust_prepareCCtxForSimpleCompression(cctx, src_size, level) };
if ERR_isError(prepare) {
return prepare;
}
let result = unsafe { compress_frame(dst, dst_capacity, src, src_size, level) };
if !ERR_isError(result) {
unsafe { ZSTD_rust_markSimpleCompression2Complete(cctx) };
}
return result;
}
unsafe { ZSTD_compress2_c(cctx, dst, dst_capacity, src, src_size) }
}
}
/// Compress a streaming call when the complete input and a full output bound
/// are already available for the ordinary context configuration.
///
/// The C implementation remains the fallback for partial-output streaming,
/// `ZSTD_e_continue`/`ZSTD_e_flush`, dictionaries, and every advanced context
/// configuration. The Rust path resets the C-owned session after emitting a
/// complete frame so the same context can immediately start another frame.
#[cfg(not(test))]
#[no_mangle]
pub unsafe extern "C" fn ZSTD_compressStream2(
cctx: *mut c_void,
output: *mut ZSTD_outBuffer,
input: *mut ZSTD_inBuffer,
end_op: c_int,
) -> usize {
if cctx.is_null() || output.is_null() || input.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
let output_ref = unsafe { &mut *output };
let input_ref = unsafe { &mut *input };
if output_ref.pos > output_ref.size {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
if input_ref.pos > input_ref.size {
return ERROR(ZstdErrorCode::SrcSizeWrong);
}
if end_op == ZSTD_E_END {
let level = unsafe { ZSTD_rust_simpleCompressStream2Level(cctx.cast_const()) };
if level != c_int::MIN {
let src_size = input_ref.size - input_ref.pos;
let dst_capacity = output_ref.size - output_ref.pos;
let bound = ZSTD_compressBound(src_size);
if !ERR_isError(bound) && dst_capacity >= bound {
if dst_capacity != 0 && output_ref.dst.is_null() {
return ERROR(ZstdErrorCode::DstBufferNull);
}
if src_size != 0 && input_ref.src.is_null() {
return ERROR(ZstdErrorCode::SrcSizeWrong);
}
let dst = if output_ref.dst.is_null() {
ptr::null_mut()
} else {
unsafe { output_ref.dst.cast::<u8>().add(output_ref.pos).cast() }
};
let src = if input_ref.src.is_null() {
ptr::null()
} else {
unsafe { input_ref.src.cast::<u8>().add(input_ref.pos).cast() }
};
let result = unsafe { compress_frame(dst, dst_capacity, src, src_size, level) };
if !ERR_isError(result) {
output_ref.pos += result;
input_ref.pos = input_ref.size;
return unsafe { ZSTD_rust_resetCCtxForSimpleCompressionSession(cctx) };
}
}
}
}
unsafe { ZSTD_compressStream2_c(cctx, output, input, end_op) }
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::process::{Command, Stdio};
fn system_round_trip(compressed: &[u8]) -> Option<Vec<u8>> {
let mut child = Command::new("zstd")
.args(["-q", "-d", "-c"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
child.stdin.take()?.write_all(compressed).ok()?;
let output = child.wait_with_output().ok()?;
if !output.status.success() {
panic!(
"system zstd rejected Rust output: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Some(output.stdout)
}
fn compress_input(input: &[u8], level: c_int) -> Vec<u8> {
let capacity = crate::zstd_compress_api::ZSTD_compressBound(input.len());
assert!(!ERR_isError(capacity));
let mut output = vec![0u8; capacity];
let written = unsafe {
ZSTD_compress(
output.as_mut_ptr().cast(),
output.len(),
input.as_ptr().cast(),
input.len(),
level,
)
};
assert!(!ERR_isError(written));
output.truncate(written);
output
}
#[test]
fn reduce_table_applies_threshold_and_wrapping_subtraction() {
let mut table = [0, 1, 2, 3, 4, 5, 6, u32::MAX, 0, 0, 0, 0, 0, 0, 0, 0];
reduce_table_internal(&mut table, 3, false);
assert_eq!(&table[..8], &[0, 0, 0, 0, 0, 2, 3, u32::MAX - 3]);
let mut wrapped_threshold = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
reduce_table_internal(&mut wrapped_threshold, u32::MAX, false);
assert_eq!(wrapped_threshold[0], 0);
assert_eq!(wrapped_threshold[1], 2);
assert_eq!(wrapped_threshold[15], 16);
}
#[test]
fn reduce_table_preserves_only_the_btlazy2_mark() {
let mut ordinary = [0u32; ZSTD_ROWSIZE];
ordinary[..3].copy_from_slice(&[ZSTD_DUBT_UNSORTED_MARK, 5, 6]);
reduce_table_internal(&mut ordinary, 3, false);
assert_eq!(&ordinary[..3], &[0, 2, 3]);
let mut btlazy2 = [0u32; ZSTD_ROWSIZE];
btlazy2[..3].copy_from_slice(&[ZSTD_DUBT_UNSORTED_MARK, 5, 6]);
reduce_table_internal(&mut btlazy2, 3, true);
assert_eq!(&btlazy2[..3], &[ZSTD_DUBT_UNSORTED_MARK, 2, 3]);
}
#[test]
fn reduce_table_processes_every_cell_in_multiple_rows() {
let mut table = [0u32; ZSTD_ROWSIZE * 2];
table[0] = 2;
table[ZSTD_ROWSIZE - 1] = 8;
table[ZSTD_ROWSIZE] = 1;
table[ZSTD_ROWSIZE * 2 - 1] = u32::MAX;
reduce_table_internal(&mut table, 4, false);
assert_eq!(table[0], 0);
assert_eq!(table[ZSTD_ROWSIZE - 1], 4);
assert_eq!(table[ZSTD_ROWSIZE], 0);
assert_eq!(table[ZSTD_ROWSIZE * 2 - 1], u32::MAX - 4);
}
#[test]
fn bitmix_and_hash_salt_match_the_c_arithmetic() {
assert_eq!(bitmix(0x0123_4567_89ab_cdef, 8), 0xd498_d855_4e8d_d8cb);
assert_eq!(
advance_hash_salt(0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210),
0xe5ee_f172_e5ff_3e57
);
assert_eq!(
ZSTD_rust_advanceHashSalt(0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210),
0xe5ee_f172_e5ff_3e57
);
}
#[test]
fn index_too_close_to_max_uses_a_strict_margin_boundary() {
let threshold = ZSTD_CURRENT_MAX - ZSTD_INDEXOVERFLOW_MARGIN;
assert!(!index_too_close_to_max(threshold));
assert!(index_too_close_to_max(threshold + 1));
assert_eq!(ZSTD_rust_indexTooCloseToMax(threshold), 0);
assert_eq!(ZSTD_rust_indexTooCloseToMax(threshold + 1), 1);
}
#[test]
fn dict_too_big_uses_a_strict_chunk_size_boundary() {
assert!(!dict_too_big(0));
assert!(!dict_too_big(ZSTD_CHUNKSIZE_MAX));
assert!(dict_too_big(ZSTD_CHUNKSIZE_MAX + 1));
assert_eq!(ZSTD_rust_dictTooBig(ZSTD_CHUNKSIZE_MAX), 0);
assert_eq!(ZSTD_rust_dictTooBig(ZSTD_CHUNKSIZE_MAX + 1), 1);
}
#[test]
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]
fn one_shot_round_trip_across_block_boundaries() {
let mut input = Vec::with_capacity(128 * 1024 + 37);
for index in 0..(128 * 1024 + 37) {
input.push(((index * 17) ^ (index / 31)) as u8);
}
let compressed = compress_input(&input, 3);
if let Some(restored) = system_round_trip(&compressed) {
assert_eq!(restored, input);
}
}
#[test]
fn empty_and_short_inputs_have_valid_frames() {
for input in [b"".as_slice(), b"a", b"abcdefg", b"abcdefgh"] {
let compressed = compress_input(input, 1);
if let Some(restored) = system_round_trip(&compressed) {
assert_eq!(restored, input);
}
}
}
#[test]
fn public_error_paths_match_size_t_error_contract() {
let mut output = [0u8; 64];
let source = [1u8; 8];
assert_eq!(
unsafe {
ZSTD_compress(
output.as_mut_ptr().cast(),
0,
source.as_ptr().cast(),
source.len(),
3,
)
},
ERROR(ZstdErrorCode::DstSizeTooSmall)
);
assert_eq!(
unsafe { ZSTD_compress(ptr::null_mut(), 1, source.as_ptr().cast(), source.len(), 3) },
ERROR(ZstdErrorCode::DstBufferNull)
);
assert_eq!(
unsafe { ZSTD_compress(output.as_mut_ptr().cast(), output.len(), ptr::null(), 1, 3) },
ERROR(ZstdErrorCode::SrcSizeWrong)
);
}
}