Move the first-job versus continuation-job trace policy out of the C worker callback. Rust now decides when the external-dictionary assertion is required and orders it before the trace, while C retains the private window assertion and trace-recording leaves. Test Plan: - ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo check --tests - ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo clippy --tests -- -A clippy::manual-bits -D warnings - ulimit -v 41943040; make -j1 - ulimit -v 41943040; make -j1 -C tests test
11237 lines
372 KiB
Rust
11237 lines
372 KiB
Rust
#![allow(non_camel_case_types)]
|
|
#![allow(non_snake_case)]
|
|
#![allow(clippy::missing_safety_doc)]
|
|
#![allow(clippy::too_many_arguments)]
|
|
|
|
//! Rust-owned resource pools used by the multithreaded compressor.
|
|
//!
|
|
//! The serial LDM state, job descriptor fields, worker callback, and streaming
|
|
//! state still use private C layouts. `zstdmt_compress.c` therefore keeps
|
|
//! those operations and projects allocation/lifecycle pieces, dictionary
|
|
//! transition policy, and pure sizing policy into this module. The entry
|
|
//! points below are narrow C ABIs: buffers, `ZSTD_CCtx *` values, and job
|
|
//! descriptors remain opaque to Rust, while allocation, reuse, table-capacity
|
|
//! orchestration, dictionary branch/order policy, and sizing policy are
|
|
//! Rust-owned. C retains ownership of private descriptor fields and platform
|
|
//! synchronization.
|
|
|
|
use std::mem::{self, offset_of, size_of, MaybeUninit};
|
|
use std::os::raw::{c_int, c_uint, c_void};
|
|
use std::ptr;
|
|
use std::sync::Mutex;
|
|
|
|
use crate::bits::ZSTD_highbit32;
|
|
use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
|
|
use crate::mem::MEM_writeLE32;
|
|
use crate::zstd_compress::{ZSTD_frameProgression, ZSTD_rust_invalidateRepCodes};
|
|
use crate::zstd_compress_frame::ZSTD_rust_writeFrameHeader;
|
|
use crate::zstd_compress_params::{
|
|
ZSTD_compressionParameters, ZSTD_rust_params_updateCParamsWhileCompressing,
|
|
ZSTD_rust_params_updateCParamsWhileCompressingState,
|
|
};
|
|
|
|
const ZSTDMT_JOBLOG_MAX: c_uint = if mem::size_of::<usize>() == 4 { 29 } else { 30 };
|
|
const ZSTD_WINDOWLOG_MAX: c_uint = if mem::size_of::<usize>() == 4 { 30 } else { 31 };
|
|
#[cfg(test)]
|
|
const DEFAULT_ZSTDMT_JOBSIZE_MIN: usize = 512 << 10;
|
|
#[cfg(test)]
|
|
const DEFAULT_ZSTDMT_JOBSIZE_MAX: usize = if mem::size_of::<usize>() == 4 {
|
|
512 << 20
|
|
} else {
|
|
1024 << 20
|
|
};
|
|
|
|
const ZSTD_FAST: c_int = 1;
|
|
const ZSTD_DFAST: c_int = 2;
|
|
const ZSTD_GREEDY: c_int = 3;
|
|
const ZSTD_LAZY: c_int = 4;
|
|
const ZSTD_LAZY2: c_int = 5;
|
|
const ZSTD_BTLAZY2: c_int = 6;
|
|
const ZSTD_BTOPT: c_int = 7;
|
|
const ZSTD_BTULTRA: c_int = 8;
|
|
const ZSTD_BTULTRA2: c_int = 9;
|
|
const ZSTD_PS_ENABLE: c_int = 1;
|
|
const ZSTD_PS_DISABLE: c_int = 2;
|
|
const ZSTD_DCT_RAW_CONTENT: c_int = 1;
|
|
|
|
const RSYNC_LENGTH: usize = 32;
|
|
const RSYNC_MIN_BLOCK_LOG: usize = 17;
|
|
const RSYNC_MIN_BLOCK_SIZE: usize = 1 << RSYNC_MIN_BLOCK_LOG;
|
|
const PRIME8_BYTES: u64 = 0xCF1B_BCDC_B7A5_6463;
|
|
const ROLL_HASH_CHAR_OFFSET: u64 = 10;
|
|
const _: () = assert!(RSYNC_MIN_BLOCK_SIZE >= RSYNC_LENGTH);
|
|
|
|
type ZstdMtCompressFn =
|
|
unsafe extern "C" fn(*mut c_void, *mut c_void, usize, *const c_void, usize) -> usize;
|
|
pub type ZSTDMT_chunkProgressFn = unsafe extern "C" fn(*mut c_void, usize, usize);
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_chunkProcessResult {
|
|
pub error: usize,
|
|
pub lastBlockSize: usize,
|
|
}
|
|
|
|
/// Per-job parameter policy applied before a worker initializes its private
|
|
/// compression context. The full `ZSTD_CCtx_params` remains C-owned; only the
|
|
/// three scalar fields changed by the MT worker policy cross the ABI.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressionJobParameters {
|
|
pub checksumFlag: c_int,
|
|
pub ldmEnable: c_int,
|
|
pub nbWorkers: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_compressionJobParameters, checksumFlag) == 0);
|
|
assert!(offset_of!(ZSTDMT_compressionJobParameters, ldmEnable) == size_of::<c_int>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobParameters, nbWorkers) == 2 * size_of::<c_int>());
|
|
assert!(
|
|
size_of::<ZSTDMT_compressionJobParameters>()
|
|
== 2 * size_of::<c_int>() + size_of::<c_uint>()
|
|
);
|
|
};
|
|
|
|
/// Scalar MT state used when compression parameters are updated between
|
|
/// streaming jobs. The private `ZSTDMT_CCtx` and `ZSTD_CCtx_params` layouts
|
|
/// remain in C; Rust owns the transition policy and returns the two values C
|
|
/// must publish together.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_CParamsUpdateProjection {
|
|
pub compressionLevel: c_int,
|
|
pub cctxSrcSizeHint: c_int,
|
|
pub srcSizeHint: u64,
|
|
pub dictSize: usize,
|
|
pub mode: c_int,
|
|
pub enableLdm: c_int,
|
|
pub ldmDefaultWindowLog: c_uint,
|
|
pub overrides: ZSTD_compressionParameters,
|
|
pub useRowMatchFinder: c_int,
|
|
pub exclusionMask: c_uint,
|
|
pub savedWindowLog: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_CParamsUpdateProjection, compressionLevel) == 0);
|
|
assert!(offset_of!(ZSTDMT_CParamsUpdateProjection, cctxSrcSizeHint) == size_of::<c_int>());
|
|
assert!(offset_of!(ZSTDMT_CParamsUpdateProjection, srcSizeHint) == 2 * size_of::<c_int>());
|
|
assert!(offset_of!(ZSTDMT_CParamsUpdateProjection, dictSize) == 16);
|
|
assert!(
|
|
offset_of!(ZSTDMT_CParamsUpdateProjection, mode)
|
|
== if size_of::<usize>() == 8 { 24 } else { 20 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_CParamsUpdateProjection, enableLdm)
|
|
== if size_of::<usize>() == 8 { 28 } else { 24 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_CParamsUpdateProjection, ldmDefaultWindowLog)
|
|
== if size_of::<usize>() == 8 { 32 } else { 28 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_CParamsUpdateProjection, overrides)
|
|
== if size_of::<usize>() == 8 { 36 } else { 32 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_CParamsUpdateProjection, useRowMatchFinder)
|
|
== if size_of::<usize>() == 8 { 64 } else { 60 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_CParamsUpdateProjection, exclusionMask)
|
|
== if size_of::<usize>() == 8 { 68 } else { 64 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_CParamsUpdateProjection, savedWindowLog)
|
|
== if size_of::<usize>() == 8 { 72 } else { 68 }
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_CParamsUpdateProjection>()
|
|
== if size_of::<usize>() == 8 { 80 } else { 72 }
|
|
);
|
|
};
|
|
|
|
/// Result of the MT parameter transition. Keeping the requested level next
|
|
/// to the derived compression parameters makes the C publication explicit:
|
|
/// C retains the private context, while Rust decides the complete scalar
|
|
/// state for the next job.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_CParamsUpdateResult {
|
|
pub compressionLevel: c_int,
|
|
pub cParams: ZSTD_compressionParameters,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_CParamsUpdateResult, compressionLevel) == 0);
|
|
assert!(offset_of!(ZSTDMT_CParamsUpdateResult, cParams) == size_of::<c_int>());
|
|
assert!(
|
|
size_of::<ZSTDMT_CParamsUpdateResult>()
|
|
== size_of::<c_int>() + size_of::<ZSTD_compressionParameters>()
|
|
);
|
|
};
|
|
|
|
#[inline]
|
|
fn update_cparams_while_compressing(
|
|
projection: ZSTDMT_CParamsUpdateProjection,
|
|
) -> ZSTDMT_CParamsUpdateResult {
|
|
let c_params = ZSTD_rust_params_updateCParamsWhileCompressing(
|
|
ZSTD_rust_params_updateCParamsWhileCompressingState {
|
|
compressionLevel: projection.compressionLevel,
|
|
cctxSrcSizeHint: projection.cctxSrcSizeHint,
|
|
srcSizeHint: projection.srcSizeHint,
|
|
dictSize: projection.dictSize,
|
|
mode: projection.mode,
|
|
enableLdm: projection.enableLdm,
|
|
ldmDefaultWindowLog: projection.ldmDefaultWindowLog,
|
|
overrides: projection.overrides,
|
|
useRowMatchFinder: projection.useRowMatchFinder,
|
|
exclusionMask: projection.exclusionMask,
|
|
savedWindowLog: projection.savedWindowLog,
|
|
},
|
|
);
|
|
ZSTDMT_CParamsUpdateResult {
|
|
compressionLevel: projection.compressionLevel,
|
|
cParams: c_params,
|
|
}
|
|
}
|
|
|
|
/// Derive and publish the scalar MT parameter transition for the next job.
|
|
/// C keeps the private parameter object and performs the two final writes;
|
|
/// Rust owns the rule that the requested level travels with the derived
|
|
/// parameters and that the active frame window log is preserved.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_updateCParamsWhileCompressing(
|
|
projection: ZSTDMT_CParamsUpdateProjection,
|
|
) -> ZSTDMT_CParamsUpdateResult {
|
|
update_cparams_while_compressing(projection)
|
|
}
|
|
|
|
/// Scalar inputs for worker-resource acquisition. The worker descriptor,
|
|
/// synchronization, pools, and destination storage remain private to C;
|
|
/// Rust owns the acquisition/publication order and error short-circuiting.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressionJobResourceProjection {
|
|
pub ldmEnabled: c_int,
|
|
pub hasDestination: c_int,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_compressionJobResourceProjection, ldmEnabled) == 0);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobResourceProjection, hasDestination) == size_of::<c_int>()
|
|
);
|
|
assert!(size_of::<ZSTDMT_compressionJobResourceProjection>() == 2 * size_of::<c_int>());
|
|
};
|
|
|
|
pub type ZSTDMT_compressionJobResourceVoidFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_compressionJobResourceStepFn = unsafe extern "C" fn(*mut c_void) -> usize;
|
|
pub type ZSTDMT_compressionJobResourceReadyFn = unsafe extern "C" fn(*mut c_void) -> c_int;
|
|
|
|
#[inline]
|
|
fn prepare_compression_job_parameters(
|
|
job_id: c_uint,
|
|
checksum_flag: c_int,
|
|
_ldm_enable: c_int,
|
|
_nb_workers: c_uint,
|
|
) -> ZSTDMT_compressionJobParameters {
|
|
ZSTDMT_compressionJobParameters {
|
|
/* The first job writes the frame checksum; later jobs have their
|
|
* checksum accounted for by the serial MT state. */
|
|
checksumFlag: if job_id == 0 { checksum_flag } else { 0 },
|
|
/* LDM sequences are generated by the serial state, not per worker. */
|
|
ldmEnable: ZSTD_PS_DISABLE,
|
|
/* Worker contexts are initialized as single-threaded contexts. */
|
|
nbWorkers: 0,
|
|
}
|
|
}
|
|
|
|
/// Apply the pure per-worker MT parameter policy while C retains the complete
|
|
/// compression-parameter structure and all codec initialization.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_prepareCompressionJobParameters(
|
|
jobID: c_uint,
|
|
checksumFlag: c_int,
|
|
ldmEnable: c_int,
|
|
nbWorkers: c_uint,
|
|
) -> ZSTDMT_compressionJobParameters {
|
|
prepare_compression_job_parameters(jobID, checksumFlag, ldmEnable, nbWorkers)
|
|
}
|
|
|
|
#[inline]
|
|
fn compression_job_acquire_resources_with<A, S, C, B, R, P>(
|
|
projection: ZSTDMT_compressionJobResourceProjection,
|
|
mut acquire_cctx: A,
|
|
mut acquire_raw_seq_store: S,
|
|
mut has_cctx: C,
|
|
mut acquire_destination_buffer: B,
|
|
mut has_raw_seq_store: R,
|
|
mut publish_destination: P,
|
|
) -> usize
|
|
where
|
|
A: FnMut(),
|
|
S: FnMut(),
|
|
C: FnMut() -> bool,
|
|
B: FnMut() -> usize,
|
|
R: FnMut() -> bool,
|
|
P: FnMut(),
|
|
{
|
|
/* Keep both pool gets before checking the CCtx, matching the C worker
|
|
* cleanup path that releases every resource acquired for the job. */
|
|
acquire_cctx();
|
|
acquire_raw_seq_store();
|
|
if !has_cctx() {
|
|
return ERROR(ZstdErrorCode::MemoryAllocation);
|
|
}
|
|
|
|
if projection.hasDestination == 0 {
|
|
let error = acquire_destination_buffer();
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
}
|
|
publish_destination();
|
|
|
|
if projection.ldmEnabled != 0 && !has_raw_seq_store() {
|
|
return ERROR(ZstdErrorCode::MemoryAllocation);
|
|
}
|
|
0
|
|
}
|
|
|
|
/// Own worker-resource acquisition policy while C retains pool, descriptor,
|
|
/// synchronization, and allocator operations behind callbacks. The order is
|
|
/// intentionally: acquire both worker resources, validate the CCtx, acquire a
|
|
/// destination only when absent, publish its scalar destination projection,
|
|
/// then validate the LDM sequence store.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_compressionJobAcquireResources(
|
|
projection: *const ZSTDMT_compressionJobResourceProjection,
|
|
opaque: *mut c_void,
|
|
acquire_cctx: Option<ZSTDMT_compressionJobResourceVoidFn>,
|
|
acquire_raw_seq_store: Option<ZSTDMT_compressionJobResourceVoidFn>,
|
|
has_cctx: Option<ZSTDMT_compressionJobResourceReadyFn>,
|
|
acquire_destination_buffer: Option<ZSTDMT_compressionJobResourceStepFn>,
|
|
has_raw_seq_store: Option<ZSTDMT_compressionJobResourceReadyFn>,
|
|
publish_destination: Option<ZSTDMT_compressionJobResourceVoidFn>,
|
|
) -> usize {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
let (
|
|
Some(acquire_cctx),
|
|
Some(acquire_raw_seq_store),
|
|
Some(has_cctx),
|
|
Some(acquire_destination_buffer),
|
|
Some(has_raw_seq_store),
|
|
Some(publish_destination),
|
|
) = (
|
|
acquire_cctx,
|
|
acquire_raw_seq_store,
|
|
has_cctx,
|
|
acquire_destination_buffer,
|
|
has_raw_seq_store,
|
|
publish_destination,
|
|
)
|
|
else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
if opaque.is_null() {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
}
|
|
|
|
unsafe {
|
|
compression_job_acquire_resources_with(
|
|
projection,
|
|
|| acquire_cctx(opaque),
|
|
|| acquire_raw_seq_store(opaque),
|
|
|| has_cctx(opaque) != 0,
|
|
|| acquire_destination_buffer(opaque),
|
|
|| has_raw_seq_store(opaque) != 0,
|
|
|| publish_destination(opaque),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Scalar job state used by the Rust compression-job scheduler.
|
|
///
|
|
/// The job descriptor, pools, synchronization, and codec state remain
|
|
/// private to C. Rust uses the frame-position flags and a scalar header
|
|
/// projection to choose the non-first-job header stage.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressionJobProjection {
|
|
pub firstJob: c_uint,
|
|
pub lastJob: c_uint,
|
|
pub frameHeaderState: *const ZSTDMT_compressionJobFrameHeaderState,
|
|
pub sequenceState: *const ZSTDMT_compressionJobSequenceState,
|
|
}
|
|
|
|
/// Indirection into the worker-owned state used by the external-sequence
|
|
/// gate. Rust sees only pointers to the opaque CCtx and the already-projected
|
|
/// raw-sequence store; the private worker-state layout remains in C.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressionJobSequenceState {
|
|
pub cctx: *mut *mut c_void,
|
|
pub rawSeqStore: *const ZstdMtRawSeqStore,
|
|
pub ldmEnabled: c_int,
|
|
}
|
|
|
|
/// Scalar projection for the non-first MT job frame header. The destination,
|
|
/// stage, and repcodes point at the worker-owned C storage; all header
|
|
/// serialization and repcode invalidation happen in Rust.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressionJobFrameHeaderState {
|
|
dst: *mut c_void,
|
|
dst_capacity: usize,
|
|
stage: *mut c_int,
|
|
no_dict_id_flag: c_int,
|
|
checksum_flag: c_int,
|
|
content_size_flag: c_int,
|
|
format: c_int,
|
|
window_log: c_uint,
|
|
pledged_src_size_plus_one: u64,
|
|
dict_id: c_uint,
|
|
rep_codes: *mut c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_compressionJobProjection, firstJob) == 0);
|
|
assert!(offset_of!(ZSTDMT_compressionJobProjection, lastJob) == size_of::<c_uint>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobProjection, frameHeaderState) == size_of::<[c_uint; 2]>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobProjection, sequenceState)
|
|
== size_of::<[c_uint; 2]>() + size_of::<usize>()
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_compressionJobProjection>()
|
|
== size_of::<[c_uint; 2]>() + 2 * size_of::<usize>()
|
|
);
|
|
assert!(offset_of!(ZSTDMT_compressionJobSequenceState, cctx) == 0);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobSequenceState, rawSeqStore) == size_of::<usize>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobSequenceState, ldmEnabled) == 2 * size_of::<usize>()
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_compressionJobSequenceState>()
|
|
== if size_of::<usize>() == 8 { 24 } else { 12 }
|
|
);
|
|
assert!(offset_of!(ZSTDMT_compressionJobFrameHeaderState, dst) == 0);
|
|
assert!(offset_of!(ZSTDMT_compressionJobFrameHeaderState, dst_capacity) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobFrameHeaderState, stage) == 2 * size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFrameHeaderState, no_dict_id_flag)
|
|
== 3 * size_of::<usize>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFrameHeaderState, checksum_flag)
|
|
== 3 * size_of::<usize>() + size_of::<c_int>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFrameHeaderState, content_size_flag)
|
|
== 3 * size_of::<usize>() + 2 * size_of::<c_int>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFrameHeaderState, format)
|
|
== 3 * size_of::<usize>() + 3 * size_of::<c_int>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFrameHeaderState, window_log)
|
|
== 3 * size_of::<usize>() + 4 * size_of::<c_int>()
|
|
);
|
|
assert!(
|
|
offset_of!(
|
|
ZSTDMT_compressionJobFrameHeaderState,
|
|
pledged_src_size_plus_one
|
|
) == ZSTDMT_HEADER_PLEDGED_OFFSET
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFrameHeaderState, dict_id)
|
|
== ZSTDMT_HEADER_PLEDGED_OFFSET + size_of::<u64>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFrameHeaderState, rep_codes)
|
|
== ZSTDMT_HEADER_REP_CODES_OFFSET
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_compressionJobFrameHeaderState>()
|
|
== if size_of::<usize>() == 8 { 72 } else { 48 }
|
|
);
|
|
};
|
|
|
|
const ZSTD_COMPRESSION_STAGE_CREATED: c_int = 0;
|
|
const ZSTD_COMPRESSION_STAGE_INIT: c_int = 1;
|
|
const ZSTD_COMPRESSION_STAGE_ONGOING: c_int = 2;
|
|
const ZSTDMT_HEADER_PLEDGED_OFFSET: usize = if size_of::<usize>() == 8 {
|
|
size_of::<[usize; 6]>()
|
|
} else {
|
|
size_of::<[usize; 8]>()
|
|
};
|
|
const ZSTDMT_HEADER_REP_CODES_OFFSET: usize = if size_of::<usize>() == 8 {
|
|
size_of::<[usize; 8]>()
|
|
} else {
|
|
size_of::<[usize; 11]>()
|
|
};
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_writeFrameHeader(
|
|
state: *const ZSTDMT_compressionJobFrameHeaderState,
|
|
) -> usize {
|
|
if state.is_null() {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
}
|
|
let state = unsafe { &*state };
|
|
if state.stage.is_null() || state.rep_codes.is_null() {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
}
|
|
if unsafe { *state.stage } == ZSTD_COMPRESSION_STAGE_CREATED {
|
|
return ERROR(ZstdErrorCode::StageWrong);
|
|
}
|
|
|
|
let header_size = if unsafe { *state.stage } == ZSTD_COMPRESSION_STAGE_INIT {
|
|
let header_size = unsafe {
|
|
ZSTD_rust_writeFrameHeader(
|
|
state.dst,
|
|
state.dst_capacity,
|
|
state.no_dict_id_flag,
|
|
state.checksum_flag,
|
|
state.content_size_flag,
|
|
state.format,
|
|
state.window_log,
|
|
state.pledged_src_size_plus_one.wrapping_sub(1),
|
|
state.dict_id,
|
|
)
|
|
};
|
|
if ERR_isError(header_size) {
|
|
return header_size;
|
|
}
|
|
if header_size > state.dst_capacity {
|
|
return ERROR(ZstdErrorCode::DstSizeTooSmall);
|
|
}
|
|
unsafe { *state.stage = ZSTD_COMPRESSION_STAGE_ONGOING };
|
|
header_size
|
|
} else {
|
|
0
|
|
};
|
|
|
|
unsafe { ZSTD_rust_invalidateRepCodes(state.rep_codes) };
|
|
header_size
|
|
}
|
|
|
|
pub type ZSTDMT_compressionJobStepFn = unsafe extern "C" fn(*mut c_void) -> usize;
|
|
pub type ZSTDMT_compressionJobVoidFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_compressionJobCompressFn =
|
|
unsafe extern "C" fn(*mut c_void, c_uint) -> ZSTDMT_chunkProcessResult;
|
|
pub type ZSTDMT_compressionJobSizeFn = unsafe extern "C" fn(*mut c_void, usize);
|
|
pub type ZSTDMT_compressionJobSetParameterFn = unsafe extern "C" fn(*mut c_void, c_int) -> usize;
|
|
pub type ZSTDMT_compressionJobApplySequencesFn =
|
|
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, usize);
|
|
|
|
/// Live fields and synchronization callbacks for one worker's intermediate
|
|
/// compression progress. The private job descriptor and pthread objects
|
|
/// remain in C; Rust owns the publication order and wrapping counter update.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZSTDMT_compressionJobProgressProjection {
|
|
pub callbackContext: *mut c_void,
|
|
pub cSize: *mut usize,
|
|
pub consumed: *mut usize,
|
|
pub lock: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub signal: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub unlock: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub debug: Option<ZSTDMT_chunkProgressFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobProgressProjection, callbackContext) == 0
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobProgressProjection, cSize) == size_of::<*mut c_void>()
|
|
);
|
|
assert!(offset_of!(ZSTDMT_compressionJobProgressProjection, consumed)
|
|
== size_of::<*mut c_void>() + size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobProgressProjection, lock)
|
|
== size_of::<*mut c_void>() + 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobProgressProjection, signal)
|
|
== size_of::<*mut c_void>() + 2 * size_of::<usize>() + size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobProgressProjection, unlock)
|
|
== size_of::<*mut c_void>() + 2 * size_of::<usize>() + 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobProgressProjection, debug)
|
|
== size_of::<*mut c_void>() + 2 * size_of::<usize>() + 3 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_compressionJobProgressProjection>()
|
|
== size_of::<*mut c_void>() + 2 * size_of::<usize>() + 4 * size_of::<usize>());
|
|
};
|
|
|
|
/// Publish one intermediate worker block while preserving the C callback's
|
|
/// lock, update, diagnostic, signal, and unlock order.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_compressionJobProgress(
|
|
opaque: *mut c_void,
|
|
c_size: usize,
|
|
consumed: usize,
|
|
) {
|
|
let Some(state) = (unsafe {
|
|
opaque
|
|
.cast::<ZSTDMT_compressionJobProgressProjection>()
|
|
.as_ref()
|
|
}) else {
|
|
return;
|
|
};
|
|
let (Some(lock), Some(signal), Some(unlock)) = (state.lock, state.signal, state.unlock)
|
|
else {
|
|
return;
|
|
};
|
|
if state.cSize.is_null() || state.consumed.is_null() {
|
|
return;
|
|
}
|
|
|
|
unsafe {
|
|
lock(state.callbackContext);
|
|
*state.cSize = (*state.cSize).wrapping_add(c_size);
|
|
*state.consumed = consumed;
|
|
if let Some(debug) = state.debug {
|
|
debug(state.callbackContext, c_size, *state.cSize);
|
|
}
|
|
signal(state.callbackContext);
|
|
unlock(state.callbackContext);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
struct ZSTDMT_compressionJobSequenceProjection {
|
|
cctx: *mut c_void,
|
|
sequences: *mut c_void,
|
|
nbSequences: usize,
|
|
ldmEnabled: c_int,
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(not(test))]
|
|
unsafe fn compression_job_sequence_projection(
|
|
state: *const ZSTDMT_compressionJobSequenceState,
|
|
) -> Option<ZSTDMT_compressionJobSequenceProjection> {
|
|
let state = unsafe { state.as_ref() }?;
|
|
if state.cctx.is_null() || state.rawSeqStore.is_null() {
|
|
return None;
|
|
}
|
|
|
|
let cctx = unsafe { *state.cctx };
|
|
let raw_seq_store = unsafe { &*state.rawSeqStore };
|
|
Some(ZSTDMT_compressionJobSequenceProjection {
|
|
cctx,
|
|
sequences: raw_seq_store.seq.cast(),
|
|
nbSequences: raw_seq_store.size,
|
|
ldmEnabled: state.ldmEnabled,
|
|
})
|
|
}
|
|
|
|
/// Apply the worker's external-sequence policy after CCtx initialization.
|
|
/// Rust owns the non-empty gate and LDM invariant; C retains only the opaque
|
|
/// `ZSTD_referenceExternalSequences()` leaf behind the callback.
|
|
#[inline]
|
|
fn apply_compression_job_sequences_with<R>(
|
|
projection: Option<ZSTDMT_compressionJobSequenceProjection>,
|
|
mut reference_sequences: R,
|
|
) where
|
|
R: FnMut(*mut c_void, *mut c_void, usize),
|
|
{
|
|
let Some(projection) = projection else {
|
|
return;
|
|
};
|
|
if projection.nbSequences == 0 {
|
|
return;
|
|
}
|
|
|
|
debug_assert_eq!(projection.ldmEnabled, ZSTD_PS_ENABLE);
|
|
debug_assert!(!projection.cctx.is_null());
|
|
reference_sequences(projection.cctx, projection.sequences, projection.nbSequences);
|
|
}
|
|
|
|
/// Scalar view used to choose the private worker-context initialization path.
|
|
/// C retains the cdict, parameter, and CCtx objects; Rust owns the branch and
|
|
/// the rule that the frame-header projection is published only after init.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressionJobBeginProjection {
|
|
pub firstJob: c_uint,
|
|
pub hasCDict: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_compressionJobBeginProjection, firstJob) == 0);
|
|
assert!(offset_of!(ZSTDMT_compressionJobBeginProjection, hasCDict) == size_of::<c_uint>());
|
|
assert!(size_of::<ZSTDMT_compressionJobBeginProjection>() == 2 * size_of::<c_uint>());
|
|
};
|
|
|
|
#[inline]
|
|
fn begin_compression_job_with<B, F, D, P, H>(
|
|
projection: ZSTDMT_compressionJobBeginProjection,
|
|
mut begin_with_cdict: B,
|
|
mut set_force_max_window: F,
|
|
mut set_deterministic_ref_prefix: D,
|
|
mut begin_with_prefix: P,
|
|
mut publish_frame_header: H,
|
|
) -> usize
|
|
where
|
|
B: FnMut() -> usize,
|
|
F: FnMut(c_int) -> usize,
|
|
D: FnMut() -> usize,
|
|
P: FnMut() -> usize,
|
|
H: FnMut(),
|
|
{
|
|
let init_error = if projection.hasCDict != 0 {
|
|
if projection.firstJob == 0 {
|
|
return ERROR(ZstdErrorCode::StageWrong);
|
|
}
|
|
begin_with_cdict()
|
|
} else {
|
|
let force_window_error = set_force_max_window((projection.firstJob == 0) as c_int);
|
|
if ERR_isError(force_window_error) {
|
|
return force_window_error;
|
|
}
|
|
if projection.firstJob == 0 {
|
|
let error = set_deterministic_ref_prefix();
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
}
|
|
begin_with_prefix()
|
|
};
|
|
|
|
if ERR_isError(init_error) {
|
|
return init_error;
|
|
}
|
|
publish_frame_header();
|
|
init_error
|
|
}
|
|
|
|
/// Own the worker-context initialization branch and failure/publication order
|
|
/// while C keeps all private codec operations behind callbacks.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_compressionJobBegin(
|
|
projection: *const ZSTDMT_compressionJobBeginProjection,
|
|
opaque: *mut c_void,
|
|
beginWithCDict: Option<ZSTDMT_compressionJobStepFn>,
|
|
setForceMaxWindow: Option<ZSTDMT_compressionJobSetParameterFn>,
|
|
setDeterministicRefPrefix: Option<ZSTDMT_compressionJobStepFn>,
|
|
beginWithPrefix: Option<ZSTDMT_compressionJobStepFn>,
|
|
publishFrameHeader: Option<ZSTDMT_compressionJobVoidFn>,
|
|
) -> usize {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
let (
|
|
Some(begin_with_cdict),
|
|
Some(set_force_max_window),
|
|
Some(set_deterministic_ref_prefix),
|
|
Some(begin_with_prefix),
|
|
Some(publish_frame_header),
|
|
) = (
|
|
beginWithCDict,
|
|
setForceMaxWindow,
|
|
setDeterministicRefPrefix,
|
|
beginWithPrefix,
|
|
publishFrameHeader,
|
|
)
|
|
else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
|
|
begin_compression_job_with(
|
|
projection,
|
|
|| unsafe { begin_with_cdict(opaque) },
|
|
|value| unsafe { set_force_max_window(opaque, value) },
|
|
|| unsafe { set_deterministic_ref_prefix(opaque) },
|
|
|| unsafe { begin_with_prefix(opaque) },
|
|
|| unsafe { publish_frame_header(opaque) },
|
|
)
|
|
}
|
|
|
|
/// Live fields and synchronization callbacks for worker-job finish
|
|
/// publication. The private job descriptor and pthread objects remain in C;
|
|
/// Rust owns the counter updates and completion signal order.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZSTDMT_compressionJobFinishPublicationProjection {
|
|
pub callbackContext: *mut c_void,
|
|
pub cSize: *mut usize,
|
|
pub consumed: *mut usize,
|
|
pub srcSize: usize,
|
|
pub lock: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub signal: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub unlock: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub validateConsumed: Option<ZSTDMT_compressionJobSizeFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, callbackContext) == 0
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, cSize)
|
|
== size_of::<*mut c_void>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, consumed)
|
|
== size_of::<*mut c_void>() + size_of::<usize>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, srcSize)
|
|
== size_of::<*mut c_void>() + 2 * size_of::<usize>()
|
|
);
|
|
assert!(offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, lock)
|
|
== size_of::<*mut c_void>() + 3 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, signal)
|
|
== size_of::<*mut c_void>() + 3 * size_of::<usize>() + size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, unlock)
|
|
== size_of::<*mut c_void>() + 3 * size_of::<usize>() + 2 * size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishPublicationProjection, validateConsumed)
|
|
== size_of::<*mut c_void>() + 3 * size_of::<usize>() + 3 * size_of::<usize>()
|
|
);
|
|
assert!(size_of::<ZSTDMT_compressionJobFinishPublicationProjection>()
|
|
== size_of::<*mut c_void>() + 3 * size_of::<usize>() + 4 * size_of::<usize>());
|
|
};
|
|
|
|
/// C-owned leaves for the worker-job finish path.
|
|
///
|
|
/// Rust owns the error/success branch and callback order. C keeps the job
|
|
/// descriptor, pthread objects, private pools, and serial state behind the
|
|
/// remaining callbacks; publication state crosses through one live projection.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZSTDMT_compressionJobFinishProjection {
|
|
pub callbackContext: *mut c_void,
|
|
pub srcSize: usize,
|
|
pub ensureFinished: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub publication: ZSTDMT_compressionJobFinishPublicationProjection,
|
|
pub releaseSeq: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub releaseCCtx: Option<ZSTDMT_compressionJobVoidFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_compressionJobFinishProjection, callbackContext) == 0);
|
|
assert!(offset_of!(ZSTDMT_compressionJobFinishProjection, srcSize) == size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishProjection, ensureFinished) == 2 * size_of::<usize>()
|
|
);
|
|
assert!(offset_of!(ZSTDMT_compressionJobFinishProjection, publication)
|
|
== 3 * size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishProjection, releaseSeq)
|
|
== 3 * size_of::<usize>()
|
|
+ size_of::<ZSTDMT_compressionJobFinishPublicationProjection>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobFinishProjection, releaseCCtx)
|
|
== 4 * size_of::<usize>()
|
|
+ size_of::<ZSTDMT_compressionJobFinishPublicationProjection>()
|
|
);
|
|
assert!(size_of::<ZSTDMT_compressionJobFinishProjection>()
|
|
== 5 * size_of::<usize>()
|
|
+ size_of::<ZSTDMT_compressionJobFinishPublicationProjection>());
|
|
};
|
|
|
|
/// C retains the private window assertion and trace-recording leaves for an
|
|
/// MT worker. Rust owns the first-job policy that determines whether the
|
|
/// external-dictionary assertion is required before tracing.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZSTDMT_compressionJobTraceProjection {
|
|
pub callbackContext: *mut c_void,
|
|
pub firstJob: c_uint,
|
|
pub assertNoExtDict: Option<ZSTDMT_compressionJobVoidFn>,
|
|
pub trace: Option<ZSTDMT_compressionJobVoidFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_compressionJobTraceProjection, callbackContext) == 0);
|
|
assert!(offset_of!(ZSTDMT_compressionJobTraceProjection, firstJob) == size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_compressionJobTraceProjection, assertNoExtDict)
|
|
== 2 * size_of::<usize>()
|
|
);
|
|
assert!(offset_of!(ZSTDMT_compressionJobTraceProjection, trace) == 3 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_compressionJobTraceProjection>() == 4 * size_of::<usize>());
|
|
};
|
|
|
|
#[inline]
|
|
fn compression_job_trace_with<A, T>(first_job: c_uint, mut assert_no_ext_dict: A, mut trace: T)
|
|
where
|
|
A: FnMut(),
|
|
T: FnMut(),
|
|
{
|
|
if first_job == 0 {
|
|
assert_no_ext_dict();
|
|
}
|
|
trace();
|
|
}
|
|
|
|
/// Apply the worker trace policy while C retains the opaque trace leaves.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_compressionJobTrace(
|
|
projection: *const ZSTDMT_compressionJobTraceProjection,
|
|
) {
|
|
let Some(projection) = (unsafe { projection.as_ref() }) else {
|
|
return;
|
|
};
|
|
let Some(trace) = projection.trace else {
|
|
return;
|
|
};
|
|
if projection.firstJob == 0 && projection.assertNoExtDict.is_none() {
|
|
return;
|
|
}
|
|
let assert_no_ext_dict = projection.assertNoExtDict;
|
|
compression_job_trace_with(
|
|
projection.firstJob,
|
|
|| {
|
|
if let Some(assert_no_ext_dict) = assert_no_ext_dict {
|
|
unsafe { assert_no_ext_dict(projection.callbackContext) };
|
|
}
|
|
},
|
|
|| unsafe { trace(projection.callbackContext) },
|
|
);
|
|
}
|
|
|
|
pub type ZSTDMT_serialWaitForTurnFn = unsafe extern "C" fn(*mut c_void, c_uint) -> c_int;
|
|
pub type ZSTDMT_serialLdmWindowUpdateFn =
|
|
unsafe extern "C" fn(*mut c_void, *mut ZstdMtRawSeqStore, *const c_void, usize);
|
|
pub type ZSTDMT_serialLdmGenerateSequencesFn =
|
|
unsafe extern "C" fn(*mut c_void, *mut ZstdMtRawSeqStore, *const c_void, usize);
|
|
pub type ZSTDMT_serialLdmPublishWindowFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialUpdateChecksumFn = unsafe extern "C" fn(*mut c_void, *const c_void, usize);
|
|
pub type ZSTDMT_serialAdvanceFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialStateLockFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialStateWaitFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialStateBroadcastFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialStateUnlockFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialStateCallbackFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialStateSkipFn = unsafe extern "C" fn(*mut c_void, c_uint, usize);
|
|
|
|
/// Projection for the MT serial turn wait. Rust owns the lock/wait loop and
|
|
/// comparison; C retains the pthread mutex and condition variable behind the
|
|
/// callbacks. The lock intentionally remains held when this returns so the
|
|
/// caller can perform the serial turn and release it in the advance callback.
|
|
#[repr(C)]
|
|
pub struct ZSTDMT_RustSerialWaitForTurnState {
|
|
callback_context: *mut c_void,
|
|
next_job_id: *mut c_uint,
|
|
lock: Option<ZSTDMT_serialStateLockFn>,
|
|
wait: Option<ZSTDMT_serialStateWaitFn>,
|
|
job_id: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(size_of::<ZSTDMT_serialStateLockFn>() == size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_serialStateWaitFn>() == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialWaitForTurnState, callback_context) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustSerialWaitForTurnState, next_job_id) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialWaitForTurnState, lock) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialWaitForTurnState, wait) == 3 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialWaitForTurnState, job_id) == 4 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_RustSerialWaitForTurnState>() == size_of::<[usize; 5]>());
|
|
};
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateWaitForTurn(
|
|
state: *const ZSTDMT_RustSerialWaitForTurnState,
|
|
) -> c_int {
|
|
if state.is_null() {
|
|
return 0;
|
|
}
|
|
let state = unsafe { &*state };
|
|
if state.callback_context.is_null() || state.next_job_id.is_null() {
|
|
return 0;
|
|
}
|
|
let (Some(lock), Some(wait)) = (state.lock, state.wait) else {
|
|
return 0;
|
|
};
|
|
|
|
unsafe {
|
|
lock(state.callback_context);
|
|
loop {
|
|
if *state.next_job_id >= state.job_id {
|
|
break;
|
|
}
|
|
wait(state.callback_context);
|
|
}
|
|
c_int::from(*state.next_job_id == state.job_id)
|
|
}
|
|
}
|
|
|
|
/// Projection for advancing the MT serial turn. Rust owns the increment and
|
|
/// callback order; C retains the condition variable broadcast and mutex
|
|
/// unlock operations behind the callbacks.
|
|
#[repr(C)]
|
|
pub struct ZSTDMT_RustSerialAdvanceState {
|
|
callback_context: *mut c_void,
|
|
next_job_id: *mut c_uint,
|
|
broadcast: Option<ZSTDMT_serialStateBroadcastFn>,
|
|
unlock: Option<ZSTDMT_serialStateUnlockFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(size_of::<ZSTDMT_serialStateBroadcastFn>() == size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_serialStateUnlockFn>() == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialAdvanceState, callback_context) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustSerialAdvanceState, next_job_id) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialAdvanceState, broadcast) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialAdvanceState, unlock) == 3 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_RustSerialAdvanceState>() == size_of::<[usize; 4]>());
|
|
};
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateAdvance(
|
|
state: *const ZSTDMT_RustSerialAdvanceState,
|
|
) {
|
|
if state.is_null() {
|
|
return;
|
|
}
|
|
let state = unsafe { &*state };
|
|
if state.callback_context.is_null() || state.next_job_id.is_null() {
|
|
return;
|
|
}
|
|
let (Some(broadcast), Some(unlock)) = (state.broadcast, state.unlock) else {
|
|
return;
|
|
};
|
|
|
|
unsafe {
|
|
*state.next_job_id = (*state.next_job_id).wrapping_add(1);
|
|
broadcast(state.callback_context);
|
|
unlock(state.callback_context);
|
|
}
|
|
}
|
|
|
|
/// Projection for the failed-job serial cleanup path. Rust owns the lock,
|
|
/// skip decision, serial-counter publication, and callback ordering; C keeps
|
|
/// the mutex, condition variables, and private LDM-window cleanup behind the
|
|
/// callbacks.
|
|
#[repr(C)]
|
|
pub struct ZSTDMT_RustSerialEnsureFinishedState {
|
|
callback_context: *mut c_void,
|
|
next_job_id: *mut c_uint,
|
|
lock: Option<ZSTDMT_serialStateCallbackFn>,
|
|
broadcast: Option<ZSTDMT_serialStateCallbackFn>,
|
|
ldm_lock: Option<ZSTDMT_serialStateCallbackFn>,
|
|
clear_ldm_window: Option<ZSTDMT_serialStateCallbackFn>,
|
|
ldm_signal: Option<ZSTDMT_serialStateCallbackFn>,
|
|
ldm_unlock: Option<ZSTDMT_serialStateCallbackFn>,
|
|
unlock: Option<ZSTDMT_serialStateCallbackFn>,
|
|
on_skip: Option<ZSTDMT_serialStateSkipFn>,
|
|
c_size: usize,
|
|
job_id: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(size_of::<ZSTDMT_serialStateCallbackFn>() == size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_serialStateSkipFn>() == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, callback_context) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, next_job_id) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, lock) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, broadcast) == 3 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, ldm_lock) == 4 * size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialEnsureFinishedState, clear_ldm_window)
|
|
== 5 * size_of::<usize>()
|
|
);
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, ldm_signal) == 6 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, ldm_unlock) == 7 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, unlock) == size_of::<[usize; 8]>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, on_skip) == 9 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialEnsureFinishedState, c_size) == 10 * size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialEnsureFinishedState, job_id)
|
|
== 10 * size_of::<usize>() + size_of::<usize>()
|
|
);
|
|
assert!(size_of::<ZSTDMT_RustSerialEnsureFinishedState>() == size_of::<[usize; 12]>());
|
|
};
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_serialStateEnsureFinishedResult {
|
|
pub skip: c_uint,
|
|
pub nextJobID: c_uint,
|
|
}
|
|
pub type ZSTDMT_waitForJobCompleteFn = unsafe extern "C" fn(*mut c_void, c_uint, c_uint);
|
|
pub type ZSTDMT_releaseJobResourceFn = unsafe extern "C" fn(*mut c_void, c_uint);
|
|
|
|
pub type ZSTDMT_freeCCtxFn = unsafe extern "C" fn(*mut c_void);
|
|
|
|
/// Projection for MT context teardown. The context and every allocation or
|
|
/// synchronization object remain private to C; Rust owns only the ownership
|
|
/// branches and teardown ordering between callbacks.
|
|
#[repr(C)]
|
|
pub struct ZSTDMT_RustFreeCCtxState {
|
|
callback_context: *mut c_void,
|
|
provided_factory: usize,
|
|
round_buffer_present: usize,
|
|
free_factory: Option<ZSTDMT_freeCCtxFn>,
|
|
release_all_job_resources: Option<ZSTDMT_freeCCtxFn>,
|
|
free_jobs: Option<ZSTDMT_freeCCtxFn>,
|
|
free_buffer_pool: Option<ZSTDMT_freeCCtxFn>,
|
|
free_cctx_pool: Option<ZSTDMT_freeCCtxFn>,
|
|
free_seq_pool: Option<ZSTDMT_freeCCtxFn>,
|
|
free_serial_state: Option<ZSTDMT_freeCCtxFn>,
|
|
free_cdict: Option<ZSTDMT_freeCCtxFn>,
|
|
free_round_buffer: Option<ZSTDMT_freeCCtxFn>,
|
|
free_mtctx: Option<ZSTDMT_freeCCtxFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(size_of::<ZSTDMT_freeCCtxFn>() == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, callback_context) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, provided_factory) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, round_buffer_present) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_factory) == 3 * size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustFreeCCtxState, release_all_job_resources) == 4 * size_of::<usize>()
|
|
);
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_jobs) == 5 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_buffer_pool) == 6 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_cctx_pool) == 7 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_seq_pool) == 8 * (usize::BITS as usize / 8));
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_serial_state) == 9 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_cdict) == 10 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_round_buffer) == 11 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeCCtxState, free_mtctx) == 12 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_RustFreeCCtxState>() == size_of::<[usize; 13]>());
|
|
};
|
|
|
|
pub type ZSTDMT_createCCtxAllocateFn = unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void;
|
|
pub type ZSTDMT_createCCtxSetWorkersFn = unsafe extern "C" fn(*mut c_void, c_uint) -> usize;
|
|
pub type ZSTDMT_createCCtxSetInitialStateFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_createCCtxFactoryFn = unsafe extern "C" fn(*mut c_void, c_uint) -> *mut c_void;
|
|
pub type ZSTDMT_createCCtxJobsFn = unsafe extern "C" fn(*mut c_void, *mut c_uint) -> *mut c_void;
|
|
pub type ZSTDMT_createCCtxResourceFn = unsafe extern "C" fn(*mut c_void, c_uint) -> *mut c_void;
|
|
pub type ZSTDMT_createCCtxSerialInitFn = unsafe extern "C" fn(*mut c_void) -> c_int;
|
|
pub type ZSTDMT_createCCtxFreeFn = unsafe extern "C" fn(*mut c_void);
|
|
|
|
/// Scalar inputs and callbacks for MT context construction. Rust owns the
|
|
/// validation, worker clamp, and resource-construction order; C retains the
|
|
/// private context layout, allocator calls, pools, and synchronization.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZSTDMT_RustCreateCCtxProjection {
|
|
callback_context: *mut c_void,
|
|
requested_nb_workers: c_uint,
|
|
max_nb_workers: c_uint,
|
|
context_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_RustCreateCCtxProjection, callback_context) == 0);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustCreateCCtxProjection, requested_nb_workers) == size_of::<usize>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustCreateCCtxProjection, max_nb_workers)
|
|
== size_of::<usize>() + size_of::<c_uint>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustCreateCCtxProjection, context_size)
|
|
== if size_of::<usize>() == 8 { 16 } else { 12 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustCreateCCtxProjection, custom_mem)
|
|
== if size_of::<usize>() == 8 { 24 } else { 16 }
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_RustCreateCCtxProjection>()
|
|
== if size_of::<usize>() == 8 { 48 } else { 28 }
|
|
);
|
|
};
|
|
|
|
fn custom_mem_is_valid(custom_mem: ZstdCustomMem) -> bool {
|
|
custom_mem.customAlloc.is_some() == custom_mem.customFree.is_some()
|
|
}
|
|
|
|
/// Construct an MT context through the original C resource callbacks.
|
|
/// Callbacks are all invoked through the allocation and cleanup sequence even
|
|
/// after an intermediate resource returns NULL, matching the C constructor's
|
|
/// cleanup contract for partially initialized contexts.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_createCCtx(
|
|
projection: *const ZSTDMT_RustCreateCCtxProjection,
|
|
allocate: Option<ZSTDMT_createCCtxAllocateFn>,
|
|
set_workers: Option<ZSTDMT_createCCtxSetWorkersFn>,
|
|
set_initial_state: Option<ZSTDMT_createCCtxSetInitialStateFn>,
|
|
create_factory: Option<ZSTDMT_createCCtxFactoryFn>,
|
|
create_jobs: Option<ZSTDMT_createCCtxJobsFn>,
|
|
create_buffer_pool: Option<ZSTDMT_createCCtxResourceFn>,
|
|
create_cctx_pool: Option<ZSTDMT_createCCtxResourceFn>,
|
|
create_seq_pool: Option<ZSTDMT_createCCtxResourceFn>,
|
|
init_serial: Option<ZSTDMT_createCCtxSerialInitFn>,
|
|
free_context: Option<ZSTDMT_createCCtxFreeFn>,
|
|
) -> *mut c_void {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ptr::null_mut();
|
|
};
|
|
let (
|
|
Some(allocate),
|
|
Some(set_workers),
|
|
Some(set_initial_state),
|
|
Some(create_factory),
|
|
Some(create_jobs),
|
|
Some(create_buffer_pool),
|
|
Some(create_cctx_pool),
|
|
Some(create_seq_pool),
|
|
Some(init_serial),
|
|
Some(free_context),
|
|
) = (
|
|
allocate,
|
|
set_workers,
|
|
set_initial_state,
|
|
create_factory,
|
|
create_jobs,
|
|
create_buffer_pool,
|
|
create_cctx_pool,
|
|
create_seq_pool,
|
|
init_serial,
|
|
free_context,
|
|
)
|
|
else {
|
|
return ptr::null_mut();
|
|
};
|
|
|
|
if projection.callback_context.is_null()
|
|
|| projection.requested_nb_workers == 0
|
|
|| projection.max_nb_workers == 0
|
|
|| projection.context_size == 0
|
|
|| !custom_mem_is_valid(projection.custom_mem)
|
|
{
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
let nb_workers = projection
|
|
.requested_nb_workers
|
|
.min(projection.max_nb_workers);
|
|
let mut nb_jobs = projection.requested_nb_workers.wrapping_add(2);
|
|
let context = unsafe { allocate(projection.callback_context, projection.context_size) };
|
|
if context.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
// The C constructor ignores this setter's result for a valid worker
|
|
// count; preserve that exact policy before publishing the custom memory.
|
|
unsafe {
|
|
let _ = set_workers(projection.callback_context, nb_workers);
|
|
set_initial_state(projection.callback_context);
|
|
}
|
|
|
|
let factory = unsafe { create_factory(projection.callback_context, nb_workers) };
|
|
let jobs = unsafe { create_jobs(projection.callback_context, &mut nb_jobs) };
|
|
let buffer_pool = unsafe { create_buffer_pool(projection.callback_context, nb_workers) };
|
|
let cctx_pool = unsafe { create_cctx_pool(projection.callback_context, nb_workers) };
|
|
let seq_pool = unsafe { create_seq_pool(projection.callback_context, nb_workers) };
|
|
let serial_error = unsafe { init_serial(projection.callback_context) };
|
|
|
|
if factory.is_null()
|
|
|| jobs.is_null()
|
|
|| buffer_pool.is_null()
|
|
|| cctx_pool.is_null()
|
|
|| seq_pool.is_null()
|
|
|| serial_error != 0
|
|
{
|
|
unsafe { free_context(projection.callback_context) };
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
context
|
|
}
|
|
|
|
pub type ZSTDMT_resizeStepFn = unsafe extern "C" fn(*mut c_void, c_uint) -> usize;
|
|
pub type ZSTDMT_resizeResourceFn = unsafe extern "C" fn(*mut c_void, c_uint) -> *mut c_void;
|
|
|
|
/// Scalar inputs for MT pool resizing. Rust owns the resize order and
|
|
/// short-circuit policy; C retains every private pool and context mutation
|
|
/// behind these callbacks.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZSTDMT_RustResizeProjection {
|
|
callback_context: *mut c_void,
|
|
nb_workers: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_RustResizeProjection, callback_context) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustResizeProjection, nb_workers) == size_of::<usize>());
|
|
assert!(
|
|
size_of::<ZSTDMT_RustResizeProjection>() == if size_of::<usize>() == 8 { 16 } else { 8 }
|
|
);
|
|
};
|
|
|
|
/// Resize MT resources in the original order: worker factory, jobs table,
|
|
/// buffer pool, CCtx pool, sequence pool, then the parameter publication.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_resize(
|
|
projection: *const ZSTDMT_RustResizeProjection,
|
|
resize_factory: Option<ZSTDMT_resizeStepFn>,
|
|
expand_jobs: Option<ZSTDMT_resizeStepFn>,
|
|
expand_buffer_pool: Option<ZSTDMT_resizeResourceFn>,
|
|
expand_cctx_pool: Option<ZSTDMT_resizeResourceFn>,
|
|
expand_seq_pool: Option<ZSTDMT_resizeResourceFn>,
|
|
set_nb_workers: Option<ZSTDMT_resizeStepFn>,
|
|
) -> usize {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
let (
|
|
Some(resize_factory),
|
|
Some(expand_jobs),
|
|
Some(expand_buffer_pool),
|
|
Some(expand_cctx_pool),
|
|
Some(expand_seq_pool),
|
|
Some(set_nb_workers),
|
|
) = (
|
|
resize_factory,
|
|
expand_jobs,
|
|
expand_buffer_pool,
|
|
expand_cctx_pool,
|
|
expand_seq_pool,
|
|
set_nb_workers,
|
|
)
|
|
else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
if projection.callback_context.is_null() {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
}
|
|
|
|
let context = projection.callback_context;
|
|
let workers = projection.nb_workers;
|
|
let error = unsafe { resize_factory(context, workers) };
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
let error = unsafe { expand_jobs(context, workers) };
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
if unsafe { expand_buffer_pool(context, workers) }.is_null() {
|
|
return ERROR(ZstdErrorCode::MemoryAllocation);
|
|
}
|
|
if unsafe { expand_cctx_pool(context, workers) }.is_null() {
|
|
return ERROR(ZstdErrorCode::MemoryAllocation);
|
|
}
|
|
if unsafe { expand_seq_pool(context, workers) }.is_null() {
|
|
return ERROR(ZstdErrorCode::MemoryAllocation);
|
|
}
|
|
unsafe { set_nb_workers(context, workers) }
|
|
}
|
|
|
|
type ZSTDMT_waitForLdmLockFn = unsafe extern "C" fn(*mut c_void);
|
|
type ZSTDMT_waitForLdmOverlapFn = unsafe extern "C" fn(*mut c_void, *mut c_void, usize) -> c_int;
|
|
type ZSTDMT_waitForLdmWaitFn = unsafe extern "C" fn(*mut c_void);
|
|
type ZSTDMT_waitForLdmUnlockFn = unsafe extern "C" fn(*mut c_void);
|
|
|
|
/// Projection for the MT wait that protects reusable input against LDM use.
|
|
///
|
|
/// Rust owns the enabled branch and lock/overlap/wait ordering. C retains the
|
|
/// pthread mutex, condition variable, and private LDM window state.
|
|
#[repr(C)]
|
|
pub struct ZSTDMT_RustWaitForLdmState {
|
|
callback_context: *mut c_void,
|
|
buffer_start: *mut c_void,
|
|
buffer_capacity: usize,
|
|
ldm_enabled: c_int,
|
|
lock: Option<ZSTDMT_waitForLdmLockFn>,
|
|
overlaps: Option<ZSTDMT_waitForLdmOverlapFn>,
|
|
wait: Option<ZSTDMT_waitForLdmWaitFn>,
|
|
unlock: Option<ZSTDMT_waitForLdmUnlockFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(size_of::<ZSTDMT_waitForLdmLockFn>() == size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_waitForLdmOverlapFn>() == size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_waitForLdmWaitFn>() == size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_waitForLdmUnlockFn>() == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, callback_context) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, buffer_start) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, buffer_capacity) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, ldm_enabled) == 3 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, lock) == 4 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, overlaps) == 5 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, wait) == 6 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustWaitForLdmState, unlock) == 7 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_RustWaitForLdmState>() == size_of::<[usize; 8]>());
|
|
};
|
|
|
|
/// Wait until the reusable input range no longer overlaps the serial LDM
|
|
/// window, preserving the C synchronization state behind callbacks.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_waitForLdmComplete(state: *const ZSTDMT_RustWaitForLdmState) {
|
|
if state.is_null() {
|
|
return;
|
|
}
|
|
let state = unsafe { &*state };
|
|
if state.ldm_enabled == 0 || state.callback_context.is_null() {
|
|
return;
|
|
}
|
|
let (Some(lock), Some(overlaps), Some(wait), Some(unlock)) =
|
|
(state.lock, state.overlaps, state.wait, state.unlock)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
unsafe {
|
|
lock(state.callback_context);
|
|
while overlaps(
|
|
state.callback_context,
|
|
state.buffer_start,
|
|
state.buffer_capacity,
|
|
) != 0
|
|
{
|
|
wait(state.callback_context);
|
|
}
|
|
unlock(state.callback_context);
|
|
}
|
|
}
|
|
|
|
/// Scalar inputs for the MT streaming initializer. The full parameter
|
|
/// object, dictionary handles, pools, buffers, and synchronization remain
|
|
/// private to C. Rust owns the order in which the C callbacks are invoked and
|
|
/// the pure normalization/sizing decisions between those callbacks.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_initCStreamProjection {
|
|
pub requestedNbWorkers: c_uint,
|
|
pub currentNbWorkers: c_uint,
|
|
pub jobSize: usize,
|
|
pub jobSizeMin: usize,
|
|
pub jobSizeMax: usize,
|
|
pub enableLdm: c_int,
|
|
pub windowLog: c_uint,
|
|
pub chainLog: c_uint,
|
|
pub strategy: c_int,
|
|
pub overlapLog: c_int,
|
|
pub rsyncable: c_int,
|
|
pub roundBuffCapacity: usize,
|
|
pub allJobsCompleted: c_uint,
|
|
}
|
|
|
|
pub type ZSTDMT_initResizeFn = unsafe extern "C" fn(*mut c_void, c_uint) -> usize;
|
|
pub type ZSTDMT_initWaitForAllJobsFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_initReleaseAllJobResourcesFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_initApplyParametersFn = unsafe extern "C" fn(*mut c_void, usize);
|
|
pub type ZSTDMT_initDictionaryFn = unsafe extern "C" fn(*mut c_void) -> usize;
|
|
pub type ZSTDMT_initSetSizeFn = unsafe extern "C" fn(*mut c_void, usize);
|
|
pub type ZSTDMT_initSetRsyncFn = unsafe extern "C" fn(*mut c_void, u64, u64);
|
|
pub type ZSTDMT_initSetBufferSizeFn = unsafe extern "C" fn(*mut c_void, usize);
|
|
pub type ZSTDMT_initResizeRoundBufferFn = unsafe extern "C" fn(*mut c_void, usize) -> usize;
|
|
pub type ZSTDMT_initResetStreamFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_initSerialResetFn = unsafe extern "C" fn(*mut c_void, usize) -> usize;
|
|
|
|
/// Scalar dictionary classification for the MT stream initializer. C keeps
|
|
/// dictionary handles, parameter layout, prefix storage, and CDict mutation
|
|
/// behind callbacks; Rust owns the copy/borrow/raw-prefix branch selection.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_RustDictionaryProjection {
|
|
pub hasDictionary: c_uint,
|
|
pub rawContent: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_RustDictionaryProjection, hasDictionary) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustDictionaryProjection, rawContent) == size_of::<c_uint>());
|
|
assert!(size_of::<ZSTDMT_RustDictionaryProjection>() == 2 * size_of::<c_uint>());
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum PrepareDictionaryAction {
|
|
CreateCopied,
|
|
AttachBorrowed,
|
|
}
|
|
|
|
#[inline]
|
|
fn classify_prepare_dictionary(
|
|
projection: ZSTDMT_RustDictionaryProjection,
|
|
) -> PrepareDictionaryAction {
|
|
if projection.hasDictionary != 0 {
|
|
PrepareDictionaryAction::CreateCopied
|
|
} else {
|
|
PrepareDictionaryAction::AttachBorrowed
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum UpdateDictionaryAction {
|
|
AttachBorrowed,
|
|
SetRawPrefix,
|
|
CreateReferenced,
|
|
}
|
|
|
|
#[inline]
|
|
fn classify_update_dictionary(
|
|
projection: ZSTDMT_RustDictionaryProjection,
|
|
) -> UpdateDictionaryAction {
|
|
if projection.hasDictionary == 0 {
|
|
UpdateDictionaryAction::AttachBorrowed
|
|
} else if projection.rawContent != 0 {
|
|
UpdateDictionaryAction::SetRawPrefix
|
|
} else {
|
|
UpdateDictionaryAction::CreateReferenced
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn prepare_c_stream_dictionary_with<R, C, A>(
|
|
projection: ZSTDMT_RustDictionaryProjection,
|
|
mut release_local: R,
|
|
mut create_copied: C,
|
|
mut attach_borrowed: A,
|
|
) -> usize
|
|
where
|
|
R: FnMut(),
|
|
C: FnMut() -> usize,
|
|
A: FnMut(),
|
|
{
|
|
release_local();
|
|
match classify_prepare_dictionary(projection) {
|
|
PrepareDictionaryAction::CreateCopied => create_copied(),
|
|
PrepareDictionaryAction::AttachBorrowed => {
|
|
attach_borrowed();
|
|
0
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn update_c_stream_dictionary_with<C, A, P, R>(
|
|
projection: ZSTDMT_RustDictionaryProjection,
|
|
mut clear_local: C,
|
|
mut attach_borrowed: A,
|
|
mut set_raw_prefix: P,
|
|
mut create_referenced: R,
|
|
) -> usize
|
|
where
|
|
C: FnMut(),
|
|
A: FnMut(),
|
|
P: FnMut(),
|
|
R: FnMut() -> usize,
|
|
{
|
|
clear_local();
|
|
match classify_update_dictionary(projection) {
|
|
UpdateDictionaryAction::AttachBorrowed => {
|
|
attach_borrowed();
|
|
0
|
|
}
|
|
UpdateDictionaryAction::SetRawPrefix => {
|
|
set_raw_prefix();
|
|
0
|
|
}
|
|
UpdateDictionaryAction::CreateReferenced => create_referenced(),
|
|
}
|
|
}
|
|
|
|
/// Apply the MT stream dictionary transition policy while C retains all
|
|
/// private CDict allocation and context publication operations.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_prepareCStreamDictionary(
|
|
projection: *const ZSTDMT_RustDictionaryProjection,
|
|
opaque: *mut c_void,
|
|
release_local: Option<ZSTDMT_initResetStreamFn>,
|
|
create_copied: Option<ZSTDMT_initDictionaryFn>,
|
|
attach_borrowed: Option<ZSTDMT_initResetStreamFn>,
|
|
) -> usize {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
let (Some(release_local), Some(create_copied), Some(attach_borrowed)) =
|
|
(release_local, create_copied, attach_borrowed)
|
|
else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
if opaque.is_null() {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
}
|
|
|
|
prepare_c_stream_dictionary_with(
|
|
projection,
|
|
|| unsafe { release_local(opaque) },
|
|
|| unsafe { create_copied(opaque) },
|
|
|| unsafe { attach_borrowed(opaque) },
|
|
)
|
|
}
|
|
|
|
/// Apply the MT stream dictionary update policy while C retains private raw
|
|
/// prefix and CDict state. The clear step always precedes the selected branch.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_updateCStreamDictionary(
|
|
projection: *const ZSTDMT_RustDictionaryProjection,
|
|
opaque: *mut c_void,
|
|
clear_local: Option<ZSTDMT_initResetStreamFn>,
|
|
attach_borrowed: Option<ZSTDMT_initResetStreamFn>,
|
|
set_raw_prefix: Option<ZSTDMT_initResetStreamFn>,
|
|
create_referenced: Option<ZSTDMT_initDictionaryFn>,
|
|
) -> usize {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
let (Some(clear_local), Some(attach_borrowed), Some(set_raw_prefix), Some(create_referenced)) = (
|
|
clear_local,
|
|
attach_borrowed,
|
|
set_raw_prefix,
|
|
create_referenced,
|
|
) else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
if opaque.is_null() {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
}
|
|
|
|
update_c_stream_dictionary_with(
|
|
projection,
|
|
|| unsafe { clear_local(opaque) },
|
|
|| unsafe { attach_borrowed(opaque) },
|
|
|| unsafe { set_raw_prefix(opaque) },
|
|
|| unsafe { create_referenced(opaque) },
|
|
)
|
|
}
|
|
|
|
type ZSTDMT_resetStreamCallbackFn = unsafe extern "C" fn(*mut c_void);
|
|
/// Scalar inputs for the MT serial-state reset. The private C parameter,
|
|
/// serial-state, LDM-state, and sequence-pool layouts stay behind callbacks;
|
|
/// Rust owns only their scalar policy and ordering.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_RustSerialResetProjection {
|
|
pub enableLdm: c_int,
|
|
pub checksumEnabled: c_int,
|
|
pub jobSize: usize,
|
|
pub windowLog: c_uint,
|
|
pub strategy: c_int,
|
|
pub hashLog: c_uint,
|
|
pub bucketSizeLog: c_uint,
|
|
pub minMatchLength: c_uint,
|
|
pub hashRateLog: c_uint,
|
|
pub previousHashLog: c_uint,
|
|
pub previousBucketSizeLog: c_uint,
|
|
pub ldmEntrySize: usize,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_RustSerialResetProjection, enableLdm) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustSerialResetProjection, checksumEnabled) == size_of::<c_int>());
|
|
assert!(offset_of!(ZSTDMT_RustSerialResetProjection, jobSize) == 8);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, windowLog)
|
|
== if size_of::<usize>() == 8 { 16 } else { 12 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, strategy)
|
|
== if size_of::<usize>() == 8 { 20 } else { 16 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, hashLog)
|
|
== if size_of::<usize>() == 8 { 24 } else { 20 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, bucketSizeLog)
|
|
== if size_of::<usize>() == 8 { 28 } else { 24 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, minMatchLength)
|
|
== if size_of::<usize>() == 8 { 32 } else { 28 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, hashRateLog)
|
|
== if size_of::<usize>() == 8 { 36 } else { 32 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, previousHashLog)
|
|
== if size_of::<usize>() == 8 { 40 } else { 36 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, previousBucketSizeLog)
|
|
== if size_of::<usize>() == 8 { 44 } else { 40 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialResetProjection, ldmEntrySize)
|
|
== if size_of::<usize>() == 8 { 48 } else { 44 }
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_RustSerialResetProjection>()
|
|
== if size_of::<usize>() == 8 { 56 } else { 48 }
|
|
);
|
|
};
|
|
|
|
pub type ZSTDMT_serialResetSetLdmParamsFn =
|
|
unsafe extern "C" fn(*mut c_void, c_int, c_uint, c_uint, c_uint, c_uint, c_uint);
|
|
pub type ZSTDMT_serialResetVoidFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialResetSetNbSeqFn = unsafe extern "C" fn(*mut c_void, usize);
|
|
pub type ZSTDMT_serialResetResizeFn =
|
|
unsafe extern "C" fn(*mut c_void, usize, usize, c_uint, c_uint) -> c_int;
|
|
pub type ZSTDMT_serialResetZeroTablesFn = unsafe extern "C" fn(*mut c_void, usize, usize);
|
|
pub type ZSTDMT_serialResetLoadRawDictionaryFn =
|
|
unsafe extern "C" fn(*mut c_void, *const c_void, usize) -> usize;
|
|
pub type ZSTDMT_serialResetSetLoadedDictEndFn = unsafe extern "C" fn(*mut c_void, c_uint);
|
|
pub type ZSTDMT_serialResetPublishFn = unsafe extern "C" fn(*mut c_void, usize);
|
|
pub type ZSTDMT_serialStateVoidFn = unsafe extern "C" fn(*mut c_void);
|
|
pub type ZSTDMT_serialStateInitFn = unsafe extern "C" fn(*mut c_void) -> c_int;
|
|
|
|
/// Dictionary inputs for the MT serial LDM preload policy. The private C
|
|
/// window/hash-table state stays behind callbacks; Rust owns eligibility,
|
|
/// reset, and the `forceWindow` publication rule.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_RustSerialDictionaryProjection {
|
|
pub dict: *const c_void,
|
|
pub dictSize: usize,
|
|
pub dictContentType: c_int,
|
|
pub forceWindow: c_int,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_RustSerialDictionaryProjection, dict) == 0);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialDictionaryProjection, dictSize) == size_of::<*const c_void>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialDictionaryProjection, dictContentType)
|
|
== size_of::<*const c_void>() + size_of::<usize>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustSerialDictionaryProjection, forceWindow)
|
|
== size_of::<*const c_void>() + size_of::<usize>() + size_of::<c_int>()
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_RustSerialDictionaryProjection>()
|
|
== size_of::<*const c_void>() + size_of::<usize>() + 2 * size_of::<c_int>()
|
|
);
|
|
};
|
|
|
|
/// Rust's LDM module already owns the scalar adjustment and maximum-sequence
|
|
/// leaves. This mirror is Rust-internal: it contains only the six scalar LDM
|
|
/// fields needed to call those leaves and is never exposed in the C ABI.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
struct ZSTDMT_serialResetLdmParameters {
|
|
enableLdm: c_int,
|
|
hashLog: c_uint,
|
|
bucketSizeLog: c_uint,
|
|
minMatchLength: c_uint,
|
|
hashRateLog: c_uint,
|
|
windowLog: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_serialResetLdmParameters, enableLdm) == 0);
|
|
assert!(offset_of!(ZSTDMT_serialResetLdmParameters, hashLog) == size_of::<c_int>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_serialResetLdmParameters, bucketSizeLog)
|
|
== size_of::<c_int>() + size_of::<c_uint>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_serialResetLdmParameters, minMatchLength)
|
|
== size_of::<c_int>() + 2 * size_of::<c_uint>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_serialResetLdmParameters, hashRateLog)
|
|
== size_of::<c_int>() + 3 * size_of::<c_uint>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_serialResetLdmParameters, windowLog)
|
|
== size_of::<c_int>() + 4 * size_of::<c_uint>()
|
|
);
|
|
assert!(size_of::<ZSTDMT_serialResetLdmParameters>() == 6 * size_of::<c_uint>());
|
|
};
|
|
|
|
#[inline]
|
|
fn serial_state_load_dictionary_with<L, S>(
|
|
projection: ZSTDMT_RustSerialDictionaryProjection,
|
|
mut load_raw_dictionary: L,
|
|
mut set_loaded_dict_end: S,
|
|
) where
|
|
L: FnMut(*const c_void, usize) -> usize,
|
|
S: FnMut(c_uint),
|
|
{
|
|
/* Match the C reset: loadedDictEnd is cleared even when no raw
|
|
* dictionary is eligible for loading. */
|
|
set_loaded_dict_end(0);
|
|
if projection.dictSize == 0 || projection.dictContentType != ZSTD_DCT_RAW_CONTENT {
|
|
return;
|
|
}
|
|
|
|
let loaded_dict_end = load_raw_dictionary(projection.dict, projection.dictSize);
|
|
if projection.forceWindow == 0 {
|
|
set_loaded_dict_end(loaded_dict_end as c_uint);
|
|
}
|
|
}
|
|
|
|
/// Apply the MT serial LDM dictionary preload policy. C retains the private
|
|
/// window/hash-table operations and publishes their computed offset through
|
|
/// the explicit callbacks.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateLoadDictionary(
|
|
projection: *const ZSTDMT_RustSerialDictionaryProjection,
|
|
opaque: *mut c_void,
|
|
load_raw_dictionary: Option<ZSTDMT_serialResetLoadRawDictionaryFn>,
|
|
set_loaded_dict_end: Option<ZSTDMT_serialResetSetLoadedDictEndFn>,
|
|
) {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return;
|
|
};
|
|
let (Some(load_raw_dictionary), Some(set_loaded_dict_end)) =
|
|
(load_raw_dictionary, set_loaded_dict_end)
|
|
else {
|
|
return;
|
|
};
|
|
if opaque.is_null() {
|
|
return;
|
|
}
|
|
|
|
unsafe {
|
|
serial_state_load_dictionary_with(
|
|
projection,
|
|
|dict, dict_size| load_raw_dictionary(opaque, dict, dict_size),
|
|
|loaded_dict_end| set_loaded_dict_end(opaque, loaded_dict_end),
|
|
);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" {
|
|
fn ZSTD_ldm_adjustParameters(
|
|
params: *mut ZSTDMT_serialResetLdmParameters,
|
|
cParams: *const ZSTD_compressionParameters,
|
|
);
|
|
fn ZSTD_ldm_getMaxNbSeq(params: ZSTDMT_serialResetLdmParameters, maxChunkSize: usize) -> usize;
|
|
}
|
|
|
|
/// Callback projection for the MT stream reset. C retains the private buffer,
|
|
/// job, flag, and progress fields; Rust owns the order in which each group is
|
|
/// reset.
|
|
#[repr(C)]
|
|
pub struct ZSTDMT_RustResetStreamState {
|
|
callback_context: *mut c_void,
|
|
reset_round_buffer: Option<ZSTDMT_resetStreamCallbackFn>,
|
|
reset_input_buffer: Option<ZSTDMT_resetStreamCallbackFn>,
|
|
reset_job_ids: Option<ZSTDMT_resetStreamCallbackFn>,
|
|
reset_frame_flags: Option<ZSTDMT_resetStreamCallbackFn>,
|
|
reset_progress: Option<ZSTDMT_resetStreamCallbackFn>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(size_of::<ZSTDMT_resetStreamCallbackFn>() == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustResetStreamState, callback_context) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustResetStreamState, reset_round_buffer) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustResetStreamState, reset_input_buffer) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustResetStreamState, reset_job_ids) == 3 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustResetStreamState, reset_frame_flags) == 4 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustResetStreamState, reset_progress) == 5 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_RustResetStreamState>() == 6 * size_of::<usize>());
|
|
};
|
|
|
|
/// Reset the MT stream state in the same order as the original C function.
|
|
/// Each callback keeps the corresponding private C layout and sentinel values
|
|
/// out of the Rust ABI.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_resetStream(state: *const ZSTDMT_RustResetStreamState) {
|
|
let Some(state) = (unsafe { state.as_ref() }) else {
|
|
return;
|
|
};
|
|
if state.callback_context.is_null() {
|
|
return;
|
|
}
|
|
let (
|
|
Some(reset_round_buffer),
|
|
Some(reset_input_buffer),
|
|
Some(reset_job_ids),
|
|
Some(reset_frame_flags),
|
|
Some(reset_progress),
|
|
) = (
|
|
state.reset_round_buffer,
|
|
state.reset_input_buffer,
|
|
state.reset_job_ids,
|
|
state.reset_frame_flags,
|
|
state.reset_progress,
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
unsafe {
|
|
reset_round_buffer(state.callback_context);
|
|
reset_input_buffer(state.callback_context);
|
|
reset_job_ids(state.callback_context);
|
|
reset_frame_flags(state.callback_context);
|
|
reset_progress(state.callback_context);
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_flushPublicationResult {
|
|
pub status: c_int,
|
|
pub toFlush: usize,
|
|
pub outputPos: usize,
|
|
pub dstFlushed: usize,
|
|
}
|
|
|
|
/// Scalar snapshot of the oldest active C-owned job used by the flush state
|
|
/// machine. C keeps the descriptor, mutex, buffer pool, and checksum state;
|
|
/// Rust decides when this snapshot can be published and completed.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_flushJobProjection {
|
|
pub consumed: usize,
|
|
pub cSize: usize,
|
|
pub srcSize: usize,
|
|
pub dstStart: *const c_void,
|
|
pub dstCapacity: usize,
|
|
pub dstFlushed: usize,
|
|
pub frameChecksumNeeded: c_uint,
|
|
}
|
|
|
|
/// Narrow projection for serializing the C-owned frame checksum. C retains
|
|
/// the serial XXH64 state and computes the checksum; Rust owns only the
|
|
/// deterministic little-endian write and destination policy.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_frameChecksumProjection {
|
|
pub dst: *mut c_void,
|
|
pub dstCapacity: usize,
|
|
pub cSize: usize,
|
|
pub checksum: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_frameChecksumProjection, dst) == 0);
|
|
assert!(offset_of!(ZSTDMT_frameChecksumProjection, dstCapacity) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_frameChecksumProjection, cSize) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_frameChecksumProjection, checksum) == 3 * size_of::<usize>());
|
|
assert!(size_of::<c_uint>() == 4);
|
|
assert!(size_of::<ZSTDMT_frameChecksumProjection>() == 4 * size_of::<usize>());
|
|
};
|
|
|
|
#[inline]
|
|
fn write_frame_checksum(projection: &ZSTDMT_frameChecksumProjection) -> usize {
|
|
if projection.dst.is_null()
|
|
|| projection.dstCapacity < 4
|
|
|| projection.cSize > projection.dstCapacity - 4
|
|
{
|
|
return ERROR(ZstdErrorCode::DstSizeTooSmall);
|
|
}
|
|
|
|
unsafe {
|
|
MEM_writeLE32(
|
|
projection.dst.cast::<u8>().add(projection.cSize).cast(),
|
|
projection.checksum,
|
|
);
|
|
}
|
|
projection.cSize + 4
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_writeFrameChecksum(
|
|
projection: *const ZSTDMT_frameChecksumProjection,
|
|
) -> usize {
|
|
let Some(projection) = (unsafe { projection.as_ref() }) else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
write_frame_checksum(projection)
|
|
}
|
|
|
|
/// Scalar view of the C-owned MT state needed after the current job is
|
|
/// published. The C adapter applies descriptor and context mutations made by
|
|
/// Rust's decisions through callbacks.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_flushContextProjection {
|
|
pub doneJobID: c_uint,
|
|
pub nextJobID: c_uint,
|
|
pub jobIDMask: c_uint,
|
|
pub jobReady: c_uint,
|
|
pub frameEnded: c_uint,
|
|
pub inBuffFilled: usize,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_flushProducedResult {
|
|
pub result: usize,
|
|
pub outputPos: usize,
|
|
pub updateAllJobsCompleted: c_uint,
|
|
pub allJobsCompleted: c_uint,
|
|
}
|
|
|
|
pub type ZSTDMT_flushProjectJobFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
job_id: c_uint,
|
|
block_to_flush: c_uint,
|
|
projection: *mut ZSTDMT_flushJobProjection,
|
|
);
|
|
pub type ZSTDMT_flushChecksumFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
job_id: c_uint,
|
|
projection: *mut ZSTDMT_flushJobProjection,
|
|
);
|
|
pub type ZSTDMT_flushUpdateJobFn =
|
|
unsafe extern "C" fn(opaque: *mut c_void, job_id: c_uint, dst_flushed: usize);
|
|
pub type ZSTDMT_flushCompleteJobFn =
|
|
unsafe extern "C" fn(opaque: *mut c_void, job_id: c_uint, src_size: usize, c_size: usize);
|
|
pub type ZSTDMT_flushWaitForAllJobsFn = unsafe extern "C" fn(opaque: *mut c_void);
|
|
pub type ZSTDMT_flushReleaseAllJobResourcesFn = unsafe extern "C" fn(opaque: *mut c_void);
|
|
|
|
/// Scalar MT state used by the outer streaming scheduler. The C context,
|
|
/// input-range ownership, and synchronization objects remain private to C;
|
|
/// Rust only reads the fields needed to choose the next operation.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressStreamContextProjection {
|
|
pub frameEnded: c_uint,
|
|
pub jobReady: c_uint,
|
|
pub inBuffStart: *mut c_void,
|
|
pub inBuffCapacity: usize,
|
|
pub inBuffFilled: usize,
|
|
pub targetSectionSize: usize,
|
|
pub rsyncable: c_int,
|
|
pub rsyncPrimePower: u64,
|
|
pub rsyncHitMask: u64,
|
|
}
|
|
|
|
/// Scalar view of the reusable C-owned input range returned by the existing
|
|
/// MT input-range seam.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_streamInputRangeProjection {
|
|
pub bufferStart: *mut c_void,
|
|
pub bufferCapacity: usize,
|
|
pub bufferFilled: usize,
|
|
}
|
|
|
|
/// Explicit projections of the public stream buffers. These deliberately do
|
|
/// not use the private C context layout or rely on a Rust view of `ZSTD_inBuffer`
|
|
/// and `ZSTD_outBuffer`.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_streamInputProjection {
|
|
pub src: *const c_void,
|
|
pub size: usize,
|
|
pub pos: usize,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_streamOutputProjection {
|
|
pub dst: *mut c_void,
|
|
pub size: usize,
|
|
pub pos: usize,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_syncPointProjection {
|
|
pub toLoad: usize,
|
|
pub flush: c_int,
|
|
}
|
|
|
|
/// Result returned by the C-owned pending-output seam after it has updated a
|
|
/// temporary output projection.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_streamFlushResult {
|
|
pub result: usize,
|
|
pub outputPos: usize,
|
|
}
|
|
|
|
/// Result of the C-owned job callback. C reports only whether its private
|
|
/// input range remains active; Rust owns the policy that publishes its local
|
|
/// `inBuffFilled` value after the callback.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_streamCreateJobResult {
|
|
pub result: usize,
|
|
pub inBuffActive: c_uint,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_streamCreateJobResult, result) == 0);
|
|
assert!(offset_of!(ZSTDMT_streamCreateJobResult, inBuffActive) == size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_streamCreateJobResult>() == 2 * size_of::<usize>());
|
|
};
|
|
|
|
/// Result of one outer MT stream scheduling pass.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_compressStreamResult {
|
|
pub result: usize,
|
|
pub inputPos: usize,
|
|
pub outputPos: usize,
|
|
pub inBuffFilled: usize,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_compressStreamResult, result) == 0);
|
|
assert!(offset_of!(ZSTDMT_compressStreamResult, inputPos) == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressStreamResult, outputPos) == 2 * size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_compressStreamResult, inBuffFilled) == 3 * size_of::<usize>());
|
|
assert!(size_of::<ZSTDMT_compressStreamResult>() == 4 * size_of::<usize>());
|
|
};
|
|
|
|
pub type ZSTDMT_streamTryGetInputRangeFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
projection: *mut ZSTDMT_streamInputRangeProjection,
|
|
) -> c_int;
|
|
pub type ZSTDMT_streamCreateJobFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
src_size: usize,
|
|
end: c_uint,
|
|
) -> ZSTDMT_streamCreateJobResult;
|
|
pub type ZSTDMT_streamFlushProducedFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
output_dst: *mut c_void,
|
|
output_size: usize,
|
|
output_pos: usize,
|
|
block_to_flush: c_uint,
|
|
end: c_uint,
|
|
) -> ZSTDMT_streamFlushResult;
|
|
|
|
const ZSTD_E_END: c_uint = 2;
|
|
const ZSTD_E_FLUSH: c_uint = 1;
|
|
const ZSTD_E_CONTINUE: c_uint = 0;
|
|
|
|
/// Scalar view of the C-owned job state needed to finish a streaming frame
|
|
/// with an empty last block. The job descriptor, buffer pool, and mutex stay
|
|
/// private to C; Rust receives only this projection and a buffer callback.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_emptyBlockJobProjection {
|
|
pub lastJob: c_uint,
|
|
pub srcSize: usize,
|
|
pub firstJob: c_uint,
|
|
pub dstStart: *mut c_void,
|
|
pub dstCapacity: usize,
|
|
pub consumed: usize,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Default)]
|
|
pub struct ZSTDMT_emptyBlockResult {
|
|
pub buffer: ZstdMtBuffer,
|
|
pub cSize: usize,
|
|
pub clearSource: c_uint,
|
|
}
|
|
|
|
pub type ZSTDMT_bufferGetFn = unsafe extern "C" fn(*mut c_void) -> ZstdMtBuffer;
|
|
|
|
/// Scalar snapshot of one C-owned job descriptor.
|
|
///
|
|
/// The C adapter fills the complete projection while holding the descriptor
|
|
/// mutex. Rust owns only the ring-scan decisions below; the descriptor layout,
|
|
/// synchronization objects, and all pointer ownership stay in C.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_jobProjection {
|
|
pub consumed: usize,
|
|
pub cSize: usize,
|
|
pub srcStart: *const c_void,
|
|
pub srcSize: usize,
|
|
pub prefixStart: *const c_void,
|
|
pub prefixSize: usize,
|
|
pub dstFlushed: usize,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_inputRange {
|
|
pub start: *const c_void,
|
|
pub size: usize,
|
|
}
|
|
|
|
/// Scalar input-buffer state used by the MT range-selection policy. C keeps
|
|
/// the input and round-buffer structs private; Rust decides only whether the
|
|
/// next target section can be reused and where that section starts.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_tryGetInputRangeProjection {
|
|
pub roundBufferStart: *const c_void,
|
|
pub roundBufferCapacity: usize,
|
|
pub roundBufferPos: usize,
|
|
pub prefixStart: *const c_void,
|
|
pub prefixSize: usize,
|
|
pub targetSectionSize: usize,
|
|
pub inUseStart: *const c_void,
|
|
pub inUseSize: usize,
|
|
}
|
|
|
|
/// Result of the MT input-range selection. Rust also owns the ordering of the
|
|
/// synchronization waits and publication callbacks that commit this result;
|
|
/// C retains the private buffer mutations and synchronization state.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_tryGetInputRangeResult {
|
|
pub ready: c_uint,
|
|
pub movePrefix: c_uint,
|
|
pub bufferStart: *mut c_void,
|
|
pub bufferCapacity: usize,
|
|
pub roundBufferPos: usize,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(offset_of!(ZSTDMT_tryGetInputRangeResult, ready) == 0);
|
|
assert!(offset_of!(ZSTDMT_tryGetInputRangeResult, movePrefix) == size_of::<c_uint>());
|
|
assert!(offset_of!(ZSTDMT_tryGetInputRangeResult, bufferStart) == 2 * size_of::<c_uint>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_tryGetInputRangeResult, bufferCapacity)
|
|
== 2 * size_of::<c_uint>() + size_of::<*mut c_void>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_tryGetInputRangeResult, roundBufferPos)
|
|
== 2 * size_of::<c_uint>() + size_of::<*mut c_void>() + size_of::<usize>()
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_tryGetInputRangeResult>()
|
|
== 2 * size_of::<c_uint>() + size_of::<*mut c_void>() + 2 * size_of::<usize>()
|
|
);
|
|
};
|
|
|
|
pub type ZSTDMT_inputRangeWaitFn =
|
|
unsafe extern "C" fn(opaque: *mut c_void, buffer_start: *mut c_void, buffer_capacity: usize);
|
|
pub type ZSTDMT_inputRangeMovePrefixFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
destination: *mut c_void,
|
|
source: *const c_void,
|
|
size: usize,
|
|
round_buffer_pos: usize,
|
|
);
|
|
pub type ZSTDMT_inputRangePublishBufferFn =
|
|
unsafe extern "C" fn(opaque: *mut c_void, buffer_start: *mut c_void, buffer_capacity: usize);
|
|
|
|
#[inline]
|
|
fn commit_input_range_with<W, M, P>(
|
|
result: ZSTDMT_tryGetInputRangeResult,
|
|
round_buffer_start: *mut c_void,
|
|
prefix_start: *const c_void,
|
|
prefix_size: usize,
|
|
opaque: *mut c_void,
|
|
mut wait_for_ldm: W,
|
|
mut move_prefix: M,
|
|
mut publish_buffer: P,
|
|
) -> c_int
|
|
where
|
|
W: FnMut(*mut c_void, *mut c_void, usize),
|
|
M: FnMut(*mut c_void, *mut c_void, *const c_void, usize, usize),
|
|
P: FnMut(*mut c_void, *mut c_void, usize),
|
|
{
|
|
if result.ready == 0 {
|
|
return 0;
|
|
}
|
|
|
|
if result.movePrefix != 0 {
|
|
wait_for_ldm(opaque, round_buffer_start, prefix_size);
|
|
move_prefix(
|
|
opaque,
|
|
round_buffer_start,
|
|
prefix_start,
|
|
prefix_size,
|
|
result.roundBufferPos,
|
|
);
|
|
}
|
|
|
|
wait_for_ldm(opaque, result.bufferStart, result.bufferCapacity);
|
|
publish_buffer(opaque, result.bufferStart, result.bufferCapacity);
|
|
1
|
|
}
|
|
|
|
/// Commit the selected MT input range in the original order. Rust owns the
|
|
/// prefix-wrap branch and the wait/copy/wait/publish sequencing; C callbacks
|
|
/// retain the private round-buffer, input-buffer, and LDM synchronization
|
|
/// operations.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_commitInputRange(
|
|
result: *const ZSTDMT_tryGetInputRangeResult,
|
|
roundBufferStart: *mut c_void,
|
|
prefixStart: *const c_void,
|
|
prefixSize: usize,
|
|
opaque: *mut c_void,
|
|
waitForLdm: Option<ZSTDMT_inputRangeWaitFn>,
|
|
movePrefix: Option<ZSTDMT_inputRangeMovePrefixFn>,
|
|
publishBuffer: Option<ZSTDMT_inputRangePublishBufferFn>,
|
|
) -> c_int {
|
|
let Some(result) = (unsafe { result.as_ref() }).copied() else {
|
|
return 0;
|
|
};
|
|
let (Some(wait_for_ldm), Some(move_prefix), Some(publish_buffer)) =
|
|
(waitForLdm, movePrefix, publishBuffer)
|
|
else {
|
|
return 0;
|
|
};
|
|
if opaque.is_null() {
|
|
return 0;
|
|
}
|
|
|
|
commit_input_range_with(
|
|
result,
|
|
roundBufferStart,
|
|
prefixStart,
|
|
prefixSize,
|
|
opaque,
|
|
|opaque, buffer_start, buffer_capacity| unsafe {
|
|
wait_for_ldm(opaque, buffer_start, buffer_capacity);
|
|
},
|
|
|opaque, destination, source, size, round_buffer_pos| unsafe {
|
|
move_prefix(opaque, destination, source, size, round_buffer_pos);
|
|
},
|
|
|opaque, buffer_start, buffer_capacity| unsafe {
|
|
publish_buffer(opaque, buffer_start, buffer_capacity);
|
|
},
|
|
)
|
|
}
|
|
|
|
pub type ZSTDMT_jobProjectionFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
job_id: c_uint,
|
|
projection: *mut ZSTDMT_jobProjection,
|
|
);
|
|
|
|
const FLUSH_PUBLICATION_OK: c_int = 0;
|
|
const FLUSH_PUBLICATION_INVALID_BOUNDS: c_int = 1;
|
|
|
|
#[inline]
|
|
fn empty_block_allocation_error() -> ZSTDMT_emptyBlockResult {
|
|
ZSTDMT_emptyBlockResult {
|
|
buffer: ZstdMtBuffer::default(),
|
|
cSize: ERROR(ZstdErrorCode::MemoryAllocation),
|
|
clearSource: 0,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn write_last_empty_block_with<F>(
|
|
projection: ZSTDMT_emptyBlockJobProjection,
|
|
mut get_buffer: F,
|
|
) -> ZSTDMT_emptyBlockResult
|
|
where
|
|
F: FnMut() -> ZstdMtBuffer,
|
|
{
|
|
debug_assert_eq!(projection.lastJob, 1);
|
|
debug_assert_eq!(projection.srcSize, 0);
|
|
debug_assert_eq!(projection.firstJob, 0);
|
|
debug_assert!(projection.dstStart.is_null());
|
|
debug_assert_eq!(projection.dstCapacity, 0);
|
|
debug_assert_eq!(projection.consumed, 0);
|
|
|
|
let buffer = get_buffer();
|
|
if buffer.start.is_null() {
|
|
return empty_block_allocation_error();
|
|
}
|
|
|
|
let c_size = unsafe {
|
|
crate::zstd_compress_frame::ZSTD_writeLastEmptyBlock(buffer.start, buffer.capacity)
|
|
};
|
|
ZSTDMT_emptyBlockResult {
|
|
buffer,
|
|
cSize: c_size,
|
|
/* The C implementation clears src immediately after acquiring the
|
|
* buffer, before serializing the block. */
|
|
clearSource: 1,
|
|
}
|
|
}
|
|
|
|
/// Acquire and serialize the terminal empty block for a C-owned MT job.
|
|
///
|
|
/// C keeps the private descriptor and applies the returned state after this
|
|
/// callback-driven operation. In particular, an allocation failure leaves
|
|
/// the source range untouched just as the original C helper did.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_writeLastEmptyBlock(
|
|
projection: *const ZSTDMT_emptyBlockJobProjection,
|
|
opaque: *mut c_void,
|
|
getBuffer: Option<ZSTDMT_bufferGetFn>,
|
|
) -> ZSTDMT_emptyBlockResult {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return empty_block_allocation_error();
|
|
};
|
|
let Some(get_buffer) = getBuffer else {
|
|
return empty_block_allocation_error();
|
|
};
|
|
unsafe { write_last_empty_block_with(projection, || get_buffer(opaque)) }
|
|
}
|
|
|
|
const CREATE_JOB_TABLE_FULL: c_uint = 0;
|
|
const CREATE_JOB_POST: c_uint = 1;
|
|
const CREATE_JOB_EMPTY: c_uint = 2;
|
|
|
|
/// Scalar MT state supplied by C for one compression-job creation attempt.
|
|
///
|
|
/// The job descriptor, input-buffer ownership, and synchronization objects
|
|
/// remain private to C. Rust owns the ring-capacity and scheduling policy over
|
|
/// this scalar view.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_createJobProjection {
|
|
pub doneJobID: c_uint,
|
|
pub nextJobID: c_uint,
|
|
pub jobIDMask: c_uint,
|
|
pub jobReady: c_uint,
|
|
pub srcStart: *const c_void,
|
|
pub srcSize: usize,
|
|
pub inBuffFilled: usize,
|
|
pub prefixStart: *const c_void,
|
|
pub prefixSize: usize,
|
|
pub targetPrefixSize: usize,
|
|
pub endFrame: c_uint,
|
|
pub checksumFlag: c_uint,
|
|
}
|
|
|
|
/// The scalar fields needed by C to initialize its private job descriptor and
|
|
/// the surrounding input state after Rust chooses to prepare a new job.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_jobInitialization {
|
|
pub srcStart: *const c_void,
|
|
pub srcSize: usize,
|
|
pub prefixStart: *const c_void,
|
|
pub prefixSize: usize,
|
|
pub nextPrefixStart: *const c_void,
|
|
pub nextPrefixSize: usize,
|
|
pub roundBuffPosDelta: usize,
|
|
pub jobNumber: c_uint,
|
|
pub firstJob: c_uint,
|
|
pub lastJob: c_uint,
|
|
pub frameChecksumNeeded: c_uint,
|
|
pub clearChecksumFlag: c_uint,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ZSTDMT_createJobResult {
|
|
pub returnCode: usize,
|
|
pub action: c_uint,
|
|
pub jobID: c_uint,
|
|
pub jobNumber: c_uint,
|
|
pub nextJobID: c_uint,
|
|
pub jobReady: c_uint,
|
|
}
|
|
|
|
pub type ZSTDMT_prepareJobFn = unsafe extern "C" fn(
|
|
opaque: *mut c_void,
|
|
job_id: c_uint,
|
|
initialization: *const ZSTDMT_jobInitialization,
|
|
);
|
|
pub type ZSTDMT_prepareFrameStateFn = unsafe extern "C" fn(opaque: *mut c_void);
|
|
pub type ZSTDMT_writeEmptyJobFn = unsafe extern "C" fn(opaque: *mut c_void, job_id: c_uint);
|
|
pub type ZSTDMT_tryAddJobFn = unsafe extern "C" fn(opaque: *mut c_void, job_id: c_uint) -> c_int;
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum CompressionJobFrameStateAction {
|
|
None,
|
|
SetFrameEnded,
|
|
SetFrameEndedAndClearChecksum,
|
|
}
|
|
|
|
#[inline]
|
|
fn classify_compression_job_frame_state(
|
|
last_job: c_uint,
|
|
clear_checksum_flag: c_uint,
|
|
) -> CompressionJobFrameStateAction {
|
|
if last_job == 0 {
|
|
CompressionJobFrameStateAction::None
|
|
} else if clear_checksum_flag == 0 {
|
|
CompressionJobFrameStateAction::SetFrameEnded
|
|
} else {
|
|
CompressionJobFrameStateAction::SetFrameEndedAndClearChecksum
|
|
}
|
|
}
|
|
|
|
/// Order the C-owned leaves that publish a newly prepared MT job.
|
|
///
|
|
/// The descriptor must become complete before the reusable input range is
|
|
/// cleared, and terminal frame state is published last. `lastJob` and
|
|
/// `clearChecksumFlag` are classified in Rust; C callbacks retain the private
|
|
/// context mutations.
|
|
#[inline]
|
|
fn prepare_compression_job_with<P, I, S, C>(
|
|
job_id: c_uint,
|
|
initialization: &ZSTDMT_jobInitialization,
|
|
mut initialize_job: P,
|
|
mut reset_input_state: I,
|
|
mut set_frame_ended: S,
|
|
mut clear_checksum_flag: C,
|
|
) where
|
|
P: FnMut(c_uint, &ZSTDMT_jobInitialization),
|
|
I: FnMut(c_uint, &ZSTDMT_jobInitialization),
|
|
S: FnMut(),
|
|
C: FnMut(),
|
|
{
|
|
let frame_state_action = classify_compression_job_frame_state(
|
|
initialization.lastJob,
|
|
initialization.clearChecksumFlag,
|
|
);
|
|
|
|
initialize_job(job_id, initialization);
|
|
reset_input_state(job_id, initialization);
|
|
match frame_state_action {
|
|
CompressionJobFrameStateAction::None => {}
|
|
CompressionJobFrameStateAction::SetFrameEnded => set_frame_ended(),
|
|
CompressionJobFrameStateAction::SetFrameEndedAndClearChecksum => {
|
|
set_frame_ended();
|
|
clear_checksum_flag();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Apply the MT job-preparation ordering while C retains the private job
|
|
/// descriptor and stream-state fields behind callbacks.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_prepareCompressionJob(
|
|
opaque: *mut c_void,
|
|
job_id: c_uint,
|
|
initialization: *const ZSTDMT_jobInitialization,
|
|
initialize_job: Option<ZSTDMT_prepareJobFn>,
|
|
reset_input_state: Option<ZSTDMT_prepareJobFn>,
|
|
set_frame_ended: Option<ZSTDMT_prepareFrameStateFn>,
|
|
clear_checksum_flag: Option<ZSTDMT_prepareFrameStateFn>,
|
|
) {
|
|
let Some(initialization) = (unsafe { initialization.as_ref() }) else {
|
|
return;
|
|
};
|
|
let (
|
|
Some(initialize_job),
|
|
Some(reset_input_state),
|
|
Some(set_frame_ended),
|
|
Some(clear_checksum_flag),
|
|
) = (
|
|
initialize_job,
|
|
reset_input_state,
|
|
set_frame_ended,
|
|
clear_checksum_flag,
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
if opaque.is_null() {
|
|
return;
|
|
}
|
|
|
|
prepare_compression_job_with(
|
|
job_id,
|
|
initialization,
|
|
|job_id, initialization| unsafe {
|
|
initialize_job(opaque, job_id, initialization);
|
|
},
|
|
|job_id, initialization| unsafe {
|
|
reset_input_state(opaque, job_id, initialization);
|
|
},
|
|
|| unsafe {
|
|
set_frame_ended(opaque);
|
|
},
|
|
|| unsafe {
|
|
clear_checksum_flag(opaque);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[inline]
|
|
fn create_job_table_full(projection: ZSTDMT_createJobProjection) -> ZSTDMT_createJobResult {
|
|
ZSTDMT_createJobResult {
|
|
action: CREATE_JOB_TABLE_FULL,
|
|
jobID: projection.nextJobID & projection.jobIDMask,
|
|
jobNumber: projection.nextJobID,
|
|
nextJobID: projection.nextJobID,
|
|
jobReady: projection.jobReady,
|
|
..ZSTDMT_createJobResult::default()
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn create_job_initialization(
|
|
projection: ZSTDMT_createJobProjection,
|
|
) -> (c_uint, ZSTDMT_jobInitialization) {
|
|
let job_number = projection.nextJobID;
|
|
let first_job = (job_number == 0) as c_uint;
|
|
let last_job = (projection.endFrame != 0) as c_uint;
|
|
let frame_checksum_needed =
|
|
(projection.checksumFlag != 0 && last_job != 0 && job_number > 0) as c_uint;
|
|
|
|
let (next_prefix_start, next_prefix_size) = if last_job != 0 {
|
|
(ptr::null(), 0)
|
|
} else {
|
|
let next_prefix_size = projection.srcSize.min(projection.targetPrefixSize);
|
|
let next_prefix_start = projection
|
|
.srcStart
|
|
.cast::<u8>()
|
|
.wrapping_add(projection.srcSize - next_prefix_size)
|
|
.cast();
|
|
(next_prefix_start, next_prefix_size)
|
|
};
|
|
|
|
(
|
|
projection.nextJobID & projection.jobIDMask,
|
|
ZSTDMT_jobInitialization {
|
|
srcStart: projection.srcStart,
|
|
srcSize: projection.srcSize,
|
|
prefixStart: projection.prefixStart,
|
|
prefixSize: projection.prefixSize,
|
|
nextPrefixStart: next_prefix_start,
|
|
nextPrefixSize: next_prefix_size,
|
|
roundBuffPosDelta: projection.srcSize,
|
|
jobNumber: job_number,
|
|
firstJob: first_job,
|
|
lastJob: last_job,
|
|
frameChecksumNeeded: frame_checksum_needed,
|
|
clearChecksumFlag: (last_job != 0 && first_job != 0) as c_uint,
|
|
},
|
|
)
|
|
}
|
|
|
|
#[inline]
|
|
fn create_compression_job_with<P, E, T>(
|
|
projection: ZSTDMT_createJobProjection,
|
|
mut prepare_job: P,
|
|
mut write_empty_job: E,
|
|
mut try_add_job: T,
|
|
) -> ZSTDMT_createJobResult
|
|
where
|
|
P: FnMut(c_uint, &ZSTDMT_jobInitialization),
|
|
E: FnMut(c_uint),
|
|
T: FnMut(c_uint) -> bool,
|
|
{
|
|
/* Match the original unsigned C comparison, including wraparound. */
|
|
if projection.nextJobID > projection.doneJobID.wrapping_add(projection.jobIDMask) {
|
|
return create_job_table_full(projection);
|
|
}
|
|
|
|
let job_id = projection.nextJobID & projection.jobIDMask;
|
|
let mut result = ZSTDMT_createJobResult {
|
|
action: CREATE_JOB_POST,
|
|
jobID: job_id,
|
|
jobNumber: projection.nextJobID,
|
|
nextJobID: projection.nextJobID,
|
|
jobReady: projection.jobReady,
|
|
..ZSTDMT_createJobResult::default()
|
|
};
|
|
|
|
if projection.jobReady == 0 {
|
|
debug_assert!(projection.inBuffFilled >= projection.srcSize);
|
|
let (_, initialization) = create_job_initialization(projection);
|
|
prepare_job(job_id, &initialization);
|
|
|
|
/* A non-first empty job is represented by the terminal empty block,
|
|
* not by a worker submission. */
|
|
if projection.srcSize == 0 && projection.nextJobID > 0 {
|
|
write_empty_job(job_id);
|
|
result.action = CREATE_JOB_EMPTY;
|
|
result.nextJobID = projection.nextJobID.wrapping_add(1);
|
|
result.jobReady = 0;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if try_add_job(job_id) {
|
|
result.nextJobID = projection.nextJobID.wrapping_add(1);
|
|
result.jobReady = 0;
|
|
} else {
|
|
result.jobReady = 1;
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Apply MT scalar scheduling policy while C retains all private descriptor,
|
|
/// pool, mutex, condition-variable, and worker-callback operations.
|
|
#[cfg(not(test))]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_createCompressionJob(
|
|
projection: *const ZSTDMT_createJobProjection,
|
|
opaque: *mut c_void,
|
|
prepareJob: Option<ZSTDMT_prepareJobFn>,
|
|
writeEmptyJob: Option<ZSTDMT_writeEmptyJobFn>,
|
|
tryAddJob: Option<ZSTDMT_tryAddJobFn>,
|
|
) -> ZSTDMT_createJobResult {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ZSTDMT_createJobResult::default();
|
|
};
|
|
let (Some(prepare_job), Some(write_empty_job), Some(try_add_job)) =
|
|
(prepareJob, writeEmptyJob, tryAddJob)
|
|
else {
|
|
return ZSTDMT_createJobResult::default();
|
|
};
|
|
|
|
create_compression_job_with(
|
|
projection,
|
|
|job_id, initialization| unsafe {
|
|
prepare_job(opaque, job_id, initialization);
|
|
},
|
|
|job_id| unsafe {
|
|
write_empty_job(opaque, job_id);
|
|
},
|
|
|job_id| unsafe { try_add_job(opaque, job_id) != 0 },
|
|
)
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn publish_finish_error(
|
|
publication: ZSTDMT_compressionJobFinishPublicationProjection,
|
|
error: usize,
|
|
) {
|
|
let (Some(lock), Some(unlock)) = (publication.lock, publication.unlock) else {
|
|
return;
|
|
};
|
|
if publication.cSize.is_null() {
|
|
return;
|
|
}
|
|
unsafe {
|
|
lock(publication.callbackContext);
|
|
*publication.cSize = error;
|
|
unlock(publication.callbackContext);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn publish_finish_last_block_size(
|
|
publication: ZSTDMT_compressionJobFinishPublicationProjection,
|
|
last_block_size: usize,
|
|
) {
|
|
let (Some(lock), Some(unlock)) = (publication.lock, publication.unlock) else {
|
|
return;
|
|
};
|
|
if publication.cSize.is_null() {
|
|
return;
|
|
}
|
|
unsafe {
|
|
lock(publication.callbackContext);
|
|
*publication.cSize = (*publication.cSize).wrapping_add(last_block_size);
|
|
unlock(publication.callbackContext);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn publish_finish_consumed(
|
|
publication: ZSTDMT_compressionJobFinishPublicationProjection,
|
|
src_size: usize,
|
|
) {
|
|
debug_assert_eq!(publication.srcSize, src_size);
|
|
if let Some(validate_consumed) = publication.validateConsumed {
|
|
unsafe { validate_consumed(publication.callbackContext, src_size) };
|
|
}
|
|
let (Some(lock), Some(unlock)) = (publication.lock, publication.unlock) else {
|
|
return;
|
|
};
|
|
if publication.consumed.is_null() {
|
|
return;
|
|
}
|
|
unsafe {
|
|
lock(publication.callbackContext);
|
|
*publication.consumed = src_size;
|
|
unlock(publication.callbackContext);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn signal_finish(publication: ZSTDMT_compressionJobFinishPublicationProjection) {
|
|
let (Some(lock), Some(signal), Some(unlock)) =
|
|
(publication.lock, publication.signal, publication.unlock)
|
|
else {
|
|
return;
|
|
};
|
|
unsafe {
|
|
lock(publication.callbackContext);
|
|
signal(publication.callbackContext);
|
|
unlock(publication.callbackContext);
|
|
}
|
|
}
|
|
|
|
/// Finish a worker job with Rust-owned branch and callback ordering.
|
|
///
|
|
/// Error publication happens before serial completion and resource release,
|
|
/// matching the original C path so failed jobs advance the serial state with
|
|
/// the published error. Success publishes the final block size only after the
|
|
/// private sequence and CCtx callbacks return. Both paths publish the full
|
|
/// consumed size and signal only after all preceding leaves have run.
|
|
#[inline]
|
|
fn finish_compression_job_with<E, F, R, C, B, P, S>(
|
|
error: usize,
|
|
last_block_size: usize,
|
|
src_size: usize,
|
|
mut publish_error: E,
|
|
mut ensure_finished: F,
|
|
mut release_seq: R,
|
|
mut release_cctx: C,
|
|
mut publish_last_block_size: B,
|
|
mut publish_consumed: P,
|
|
mut signal: S,
|
|
) where
|
|
E: FnMut(usize),
|
|
F: FnMut(),
|
|
R: FnMut(),
|
|
C: FnMut(),
|
|
B: FnMut(usize),
|
|
P: FnMut(usize),
|
|
S: FnMut(),
|
|
{
|
|
let failed = ERR_isError(error);
|
|
if failed {
|
|
publish_error(error);
|
|
}
|
|
ensure_finished();
|
|
release_seq();
|
|
release_cctx();
|
|
if !failed {
|
|
publish_last_block_size(last_block_size);
|
|
}
|
|
publish_consumed(src_size);
|
|
signal();
|
|
}
|
|
|
|
/// Run the high-level worker-job sequence while C owns all codec and
|
|
/// synchronization operations behind callbacks.
|
|
///
|
|
/// Resource acquisition and every codec-facing stage can fail with a zstd
|
|
/// error. Rust stops at the first such error, normalizes the final block size,
|
|
/// then applies the C-owned finish leaves through the Rust-owned policy above.
|
|
#[inline]
|
|
fn compression_job_with<A, P, S, B, Q, H, C, T>(
|
|
projection: ZSTDMT_compressionJobProjection,
|
|
finish: ZSTDMT_compressionJobFinishProjection,
|
|
mut acquire_resources: A,
|
|
mut prepare_parameters: P,
|
|
mut generate_sequences: S,
|
|
mut begin_job: B,
|
|
mut apply_sequences: Q,
|
|
mut write_frame_header: H,
|
|
mut compress_job: C,
|
|
mut trace_job: T,
|
|
) where
|
|
A: FnMut() -> usize,
|
|
P: FnMut(),
|
|
S: FnMut(),
|
|
B: FnMut() -> usize,
|
|
Q: FnMut(),
|
|
H: FnMut() -> usize,
|
|
C: FnMut(c_uint) -> ZSTDMT_chunkProcessResult,
|
|
T: FnMut(),
|
|
{
|
|
let (Some(ensure_finished), Some(release_seq), Some(release_cctx)) = (
|
|
finish.ensureFinished,
|
|
finish.releaseSeq,
|
|
finish.releaseCCtx,
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let mut error = acquire_resources();
|
|
let mut last_block_size = 0;
|
|
|
|
if !ERR_isError(error) {
|
|
prepare_parameters();
|
|
generate_sequences();
|
|
error = begin_job();
|
|
}
|
|
|
|
if !ERR_isError(error) {
|
|
apply_sequences();
|
|
if projection.firstJob == 0 {
|
|
error = write_frame_header();
|
|
}
|
|
}
|
|
|
|
if !ERR_isError(error) {
|
|
let result = compress_job(projection.lastJob);
|
|
if ERR_isError(result.error) {
|
|
error = result.error;
|
|
} else {
|
|
last_block_size = result.lastBlockSize;
|
|
trace_job();
|
|
}
|
|
}
|
|
|
|
if ERR_isError(error) {
|
|
last_block_size = 0;
|
|
}
|
|
let publication = finish.publication;
|
|
finish_compression_job_with(
|
|
error,
|
|
last_block_size,
|
|
finish.srcSize,
|
|
|error| unsafe { publish_finish_error(publication, error) },
|
|
|| unsafe { ensure_finished(finish.callbackContext) },
|
|
|| unsafe { release_seq(finish.callbackContext) },
|
|
|| unsafe { release_cctx(finish.callbackContext) },
|
|
|last_block_size| unsafe { publish_finish_last_block_size(publication, last_block_size) },
|
|
|src_size| unsafe { publish_finish_consumed(publication, src_size) },
|
|
|| unsafe { signal_finish(publication) },
|
|
);
|
|
}
|
|
|
|
/// Drive one ordered MT serial-state turn.
|
|
///
|
|
/// The wait callback returns with the C-owned serial mutex held. Rust then
|
|
/// performs the LDM-before-checksum policy only for the current job and always
|
|
/// invokes the advance callback exactly once, including when an earlier job
|
|
/// has already skipped this turn.
|
|
#[inline]
|
|
fn serial_state_gen_sequences_with<W, U, G, P, C, A>(
|
|
seq_store: &mut ZstdMtRawSeqStore,
|
|
src: *const c_void,
|
|
src_size: usize,
|
|
job_id: c_uint,
|
|
ldm_enabled: bool,
|
|
checksum_enabled: bool,
|
|
mut wait_for_turn: W,
|
|
mut update_ldm_window: U,
|
|
mut generate_ldm_sequences: G,
|
|
mut publish_ldm_window: P,
|
|
mut update_checksum: C,
|
|
mut advance: A,
|
|
) where
|
|
W: FnMut(c_uint) -> bool,
|
|
U: FnMut(&mut ZstdMtRawSeqStore, *const c_void, usize),
|
|
G: FnMut(&mut ZstdMtRawSeqStore, *const c_void, usize),
|
|
P: FnMut(),
|
|
C: FnMut(*const c_void, usize),
|
|
A: FnMut(),
|
|
{
|
|
if wait_for_turn(job_id) {
|
|
if ldm_enabled {
|
|
update_ldm_window(seq_store, src, src_size);
|
|
generate_ldm_sequences(seq_store, src, src_size);
|
|
publish_ldm_window();
|
|
}
|
|
if checksum_enabled && src_size != 0 {
|
|
update_checksum(src, src_size);
|
|
}
|
|
}
|
|
advance();
|
|
}
|
|
|
|
/// Own MT serial turn/skip and LDM/checksum ordering while C retains the
|
|
/// mutexes, LDM window/hash state, checksum state, and raw sequence storage.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateGenSequences(
|
|
seq_store: *mut ZstdMtRawSeqStore,
|
|
src: *const c_void,
|
|
src_size: usize,
|
|
job_id: c_uint,
|
|
ldm_enabled: c_int,
|
|
checksum_enabled: c_int,
|
|
opaque: *mut c_void,
|
|
wait_for_turn: Option<ZSTDMT_serialWaitForTurnFn>,
|
|
update_ldm_window: Option<ZSTDMT_serialLdmWindowUpdateFn>,
|
|
generate_ldm_sequences: Option<ZSTDMT_serialLdmGenerateSequencesFn>,
|
|
publish_ldm_window: Option<ZSTDMT_serialLdmPublishWindowFn>,
|
|
update_checksum: Option<ZSTDMT_serialUpdateChecksumFn>,
|
|
advance: Option<ZSTDMT_serialAdvanceFn>,
|
|
) {
|
|
let (
|
|
Some(wait_for_turn),
|
|
Some(update_ldm_window),
|
|
Some(generate_ldm_sequences),
|
|
Some(publish_ldm_window),
|
|
Some(update_checksum),
|
|
Some(advance),
|
|
) = (
|
|
wait_for_turn,
|
|
update_ldm_window,
|
|
generate_ldm_sequences,
|
|
publish_ldm_window,
|
|
update_checksum,
|
|
advance,
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
if seq_store.is_null() {
|
|
return;
|
|
}
|
|
|
|
unsafe {
|
|
serial_state_gen_sequences_with(
|
|
&mut *seq_store,
|
|
src,
|
|
src_size,
|
|
job_id,
|
|
ldm_enabled != 0,
|
|
checksum_enabled != 0,
|
|
|job_id| wait_for_turn(opaque, job_id) != 0,
|
|
|seq_store, src, src_size| update_ldm_window(opaque, seq_store, src, src_size),
|
|
|seq_store, src, src_size| {
|
|
generate_ldm_sequences(opaque, seq_store, src, src_size)
|
|
},
|
|
|| publish_ldm_window(opaque),
|
|
|src, src_size| update_checksum(opaque, src, src_size),
|
|
|| advance(opaque),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Decide whether a failed job must advance the shared serial turn.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_serialStateEnsureFinished(
|
|
next_job_id: c_uint,
|
|
job_id: c_uint,
|
|
) -> ZSTDMT_serialStateEnsureFinishedResult {
|
|
if next_job_id <= job_id {
|
|
ZSTDMT_serialStateEnsureFinishedResult {
|
|
skip: 1,
|
|
nextJobID: job_id.wrapping_add(1),
|
|
}
|
|
} else {
|
|
ZSTDMT_serialStateEnsureFinishedResult {
|
|
skip: 0,
|
|
nextJobID: next_job_id,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Own the failed-job serial cleanup order while C retains synchronization and
|
|
/// private LDM-window operations behind callbacks.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateEnsureFinishedOrchestrated(
|
|
state: *const ZSTDMT_RustSerialEnsureFinishedState,
|
|
) {
|
|
if state.is_null() {
|
|
return;
|
|
}
|
|
let state = unsafe { &*state };
|
|
if state.callback_context.is_null() || state.next_job_id.is_null() {
|
|
return;
|
|
}
|
|
let (
|
|
Some(lock),
|
|
Some(broadcast),
|
|
Some(ldm_lock),
|
|
Some(clear_ldm_window),
|
|
Some(ldm_signal),
|
|
Some(ldm_unlock),
|
|
Some(unlock),
|
|
Some(on_skip),
|
|
) = (
|
|
state.lock,
|
|
state.broadcast,
|
|
state.ldm_lock,
|
|
state.clear_ldm_window,
|
|
state.ldm_signal,
|
|
state.ldm_unlock,
|
|
state.unlock,
|
|
state.on_skip,
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
unsafe {
|
|
lock(state.callback_context);
|
|
let result = ZSTDMT_rust_serialStateEnsureFinished(*state.next_job_id, state.job_id);
|
|
if result.skip != 0 {
|
|
on_skip(state.callback_context, state.job_id, state.c_size);
|
|
*state.next_job_id = result.nextJobID;
|
|
broadcast(state.callback_context);
|
|
ldm_lock(state.callback_context);
|
|
clear_ldm_window(state.callback_context);
|
|
ldm_signal(state.callback_context);
|
|
ldm_unlock(state.callback_context);
|
|
}
|
|
unlock(state.callback_context);
|
|
}
|
|
}
|
|
|
|
/// Wait for each submitted MT job in ring order. C retains the per-job
|
|
/// mutex, condition variable, and consumed/source counters behind one
|
|
/// callback; Rust owns the ring-slot and completion progression policy.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_waitForAllJobsCompleted(
|
|
mut done_job_id: c_uint,
|
|
next_job_id: c_uint,
|
|
job_id_mask: c_uint,
|
|
opaque: *mut c_void,
|
|
wait_for_job: Option<ZSTDMT_waitForJobCompleteFn>,
|
|
) -> c_uint {
|
|
let Some(wait_for_job) = wait_for_job else {
|
|
return done_job_id;
|
|
};
|
|
while done_job_id < next_job_id {
|
|
unsafe { wait_for_job(opaque, done_job_id & job_id_mask, done_job_id) };
|
|
done_job_id = done_job_id.wrapping_add(1);
|
|
}
|
|
done_job_id
|
|
}
|
|
|
|
/// Visit every MT job slot so C can release its private descriptor resources.
|
|
/// Rust owns the deterministic inclusive ring-table iteration; C retains the
|
|
/// buffer-pool, mutex/condition, and descriptor cleanup side effects.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_releaseAllJobResources(
|
|
job_id_mask: c_uint,
|
|
opaque: *mut c_void,
|
|
release_job: Option<ZSTDMT_releaseJobResourceFn>,
|
|
) {
|
|
let Some(release_job) = release_job else {
|
|
return;
|
|
};
|
|
for job_id in 0..=job_id_mask {
|
|
unsafe { release_job(opaque, job_id) };
|
|
}
|
|
}
|
|
|
|
/// C ABI entry point for the worker-job orchestration. C supplies callbacks
|
|
/// that keep the private descriptor, pools, mutexes, and codec operations on
|
|
/// the C side of this narrow projection.
|
|
#[cfg(not(test))]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_compressionJob(
|
|
projection: *const ZSTDMT_compressionJobProjection,
|
|
finishProjection: *const ZSTDMT_compressionJobFinishProjection,
|
|
opaque: *mut c_void,
|
|
acquireResources: Option<ZSTDMT_compressionJobStepFn>,
|
|
prepareParameters: Option<ZSTDMT_compressionJobVoidFn>,
|
|
generateSequences: Option<ZSTDMT_compressionJobVoidFn>,
|
|
beginJob: Option<ZSTDMT_compressionJobStepFn>,
|
|
applySequences: Option<ZSTDMT_compressionJobApplySequencesFn>,
|
|
compressJob: Option<ZSTDMT_compressionJobCompressFn>,
|
|
traceJob: Option<ZSTDMT_compressionJobVoidFn>,
|
|
) {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return;
|
|
};
|
|
let Some(finish) = (unsafe { finishProjection.as_ref() }).copied() else {
|
|
return;
|
|
};
|
|
let (
|
|
Some(acquire_resources),
|
|
Some(prepare_parameters),
|
|
Some(generate_sequences),
|
|
Some(begin_job),
|
|
Some(apply_sequences),
|
|
Some(compress_job),
|
|
Some(trace_job),
|
|
) = (
|
|
acquireResources,
|
|
prepareParameters,
|
|
generateSequences,
|
|
beginJob,
|
|
applySequences,
|
|
compressJob,
|
|
traceJob,
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let sequence_state = projection.sequenceState;
|
|
|
|
compression_job_with(
|
|
projection,
|
|
finish,
|
|
|| unsafe { acquire_resources(opaque) },
|
|
|| unsafe { prepare_parameters(opaque) },
|
|
|| unsafe { generate_sequences(opaque) },
|
|
|| unsafe { begin_job(opaque) },
|
|
|| {
|
|
let sequence_projection =
|
|
unsafe { compression_job_sequence_projection(sequence_state) };
|
|
apply_compression_job_sequences_with(sequence_projection, |cctx, sequences, count| {
|
|
unsafe { apply_sequences(opaque, cctx, sequences, count) }
|
|
});
|
|
},
|
|
|| unsafe { ZSTDMT_rust_writeFrameHeader(projection.frameHeaderState) },
|
|
|last_job| unsafe { compress_job(opaque, last_job) },
|
|
|| unsafe { trace_job(opaque) },
|
|
);
|
|
}
|
|
|
|
#[inline]
|
|
fn normalize_mt_job_size(job_size: usize, job_size_min: usize, job_size_max: usize) -> usize {
|
|
if job_size != 0 && job_size < job_size_min {
|
|
job_size_min
|
|
} else if job_size > job_size_max {
|
|
job_size_max
|
|
} else {
|
|
job_size
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn init_serial_reset_result(status: c_int) -> usize {
|
|
if status != 0 {
|
|
ERROR(ZstdErrorCode::MemoryAllocation)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
/// Map the private C serial-reset result to the MT stream-initialization ABI
|
|
/// error. C retains serial-state allocation and mutation; Rust owns only this
|
|
/// scalar error policy.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_initSerialResetResult(status: c_int) -> usize {
|
|
init_serial_reset_result(status)
|
|
}
|
|
|
|
#[inline]
|
|
fn init_round_buffer_capacity(
|
|
projection: ZSTDMT_initCStreamProjection,
|
|
target_prefix_size: usize,
|
|
target_section_size: usize,
|
|
) -> usize {
|
|
let window_size = if projection.enableLdm == ZSTD_PS_ENABLE {
|
|
1usize << projection.windowLog as usize
|
|
} else {
|
|
0
|
|
};
|
|
let nb_slack_buffers = 2 + usize::from(target_prefix_size > 0);
|
|
let slack_size = target_section_size.wrapping_mul(nb_slack_buffers);
|
|
let nb_workers = projection.requestedNbWorkers.max(1) as usize;
|
|
let sections_size = target_section_size.wrapping_mul(nb_workers);
|
|
window_size.max(sections_size).wrapping_add(slack_size)
|
|
}
|
|
|
|
#[inline]
|
|
fn init_rsync_parameters(target_section_size: usize) -> (u64, u64) {
|
|
let job_size_kb = (target_section_size >> 10) as u32;
|
|
debug_assert!(job_size_kb >= 1);
|
|
let rsync_bits = ZSTD_highbit32(job_size_kb) + 10;
|
|
debug_assert!(rsync_bits >= (RSYNC_MIN_BLOCK_LOG + 2) as u32);
|
|
let hit_mask = 1u64.wrapping_shl(rsync_bits).wrapping_sub(1);
|
|
(hit_mask, rolling_hash_prime_power(RSYNC_LENGTH as c_uint))
|
|
}
|
|
|
|
/// Run the high-level MT streaming initialization policy while C retains all
|
|
/// private state and side effects behind callbacks. The callback order is
|
|
/// intentionally the order of the original C initializer: resize, normalize,
|
|
/// wait for all jobs, release their resources, attach the first dictionary,
|
|
/// size buffers, reset stream state,
|
|
/// update the active dictionary, and finally reset serial state.
|
|
#[inline]
|
|
fn init_c_stream_with<
|
|
Resize,
|
|
WaitForAllJobs,
|
|
ReleaseAllJobResources,
|
|
ApplyParams,
|
|
PrepareDictionary,
|
|
SetPrefixSize,
|
|
SetSectionSize,
|
|
SetRsync,
|
|
SetBufferSize,
|
|
ResizeRoundBuffer,
|
|
ResetStream,
|
|
UpdateDictionary,
|
|
SerialReset,
|
|
>(
|
|
projection: ZSTDMT_initCStreamProjection,
|
|
mut resize: Resize,
|
|
mut wait_for_all_jobs: WaitForAllJobs,
|
|
mut release_all_job_resources: ReleaseAllJobResources,
|
|
mut apply_parameters: ApplyParams,
|
|
mut prepare_dictionary: PrepareDictionary,
|
|
mut set_target_prefix_size: SetPrefixSize,
|
|
mut set_target_section_size: SetSectionSize,
|
|
mut set_rsync: SetRsync,
|
|
mut set_buffer_size: SetBufferSize,
|
|
mut resize_round_buffer: ResizeRoundBuffer,
|
|
mut reset_stream: ResetStream,
|
|
mut update_dictionary: UpdateDictionary,
|
|
mut serial_reset: SerialReset,
|
|
) -> usize
|
|
where
|
|
Resize: FnMut(c_uint) -> usize,
|
|
WaitForAllJobs: FnMut(),
|
|
ReleaseAllJobResources: FnMut(),
|
|
ApplyParams: FnMut(usize),
|
|
PrepareDictionary: FnMut() -> usize,
|
|
SetPrefixSize: FnMut(usize),
|
|
SetSectionSize: FnMut(usize),
|
|
SetRsync: FnMut(u64, u64),
|
|
SetBufferSize: FnMut(usize),
|
|
ResizeRoundBuffer: FnMut(usize) -> usize,
|
|
ResetStream: FnMut(),
|
|
UpdateDictionary: FnMut() -> usize,
|
|
SerialReset: FnMut(usize) -> usize,
|
|
{
|
|
if projection.requestedNbWorkers != projection.currentNbWorkers {
|
|
let error = resize(projection.requestedNbWorkers);
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
}
|
|
|
|
let job_size = normalize_mt_job_size(
|
|
projection.jobSize,
|
|
projection.jobSizeMin,
|
|
projection.jobSizeMax,
|
|
);
|
|
|
|
if projection.allJobsCompleted == 0 {
|
|
wait_for_all_jobs();
|
|
release_all_job_resources();
|
|
}
|
|
|
|
apply_parameters(job_size);
|
|
let error = prepare_dictionary();
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
|
|
let target_prefix_size = compute_overlap_size(
|
|
projection.windowLog,
|
|
projection.chainLog,
|
|
projection.strategy,
|
|
projection.overlapLog,
|
|
projection.enableLdm,
|
|
);
|
|
set_target_prefix_size(target_prefix_size);
|
|
|
|
let initial_target_section_size = if job_size == 0 {
|
|
1usize
|
|
<< compute_target_job_log(
|
|
projection.windowLog,
|
|
projection.chainLog,
|
|
projection.strategy,
|
|
projection.enableLdm,
|
|
) as usize
|
|
} else {
|
|
job_size
|
|
};
|
|
debug_assert!(initial_target_section_size <= projection.jobSizeMax);
|
|
set_target_section_size(initial_target_section_size);
|
|
|
|
if projection.rsyncable != 0 {
|
|
let (hit_mask, prime_power) = init_rsync_parameters(initial_target_section_size);
|
|
set_rsync(hit_mask, prime_power);
|
|
}
|
|
|
|
let target_section_size = initial_target_section_size.max(target_prefix_size);
|
|
if target_section_size != initial_target_section_size {
|
|
set_target_section_size(target_section_size);
|
|
}
|
|
|
|
set_buffer_size(crate::zstd_compress_api::ZSTD_compressBound(
|
|
target_section_size,
|
|
));
|
|
|
|
let round_capacity =
|
|
init_round_buffer_capacity(projection, target_prefix_size, target_section_size);
|
|
if projection.roundBuffCapacity < round_capacity {
|
|
let error = resize_round_buffer(round_capacity);
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
}
|
|
|
|
reset_stream();
|
|
|
|
let error = update_dictionary();
|
|
if ERR_isError(error) {
|
|
return error;
|
|
}
|
|
|
|
serial_reset(target_section_size)
|
|
}
|
|
|
|
/// Run the MT serial-state reset policy while C retains its private LDM
|
|
/// tables, dictionary/window state, checksum state, and allocator callbacks.
|
|
/// Rust prepares the scalar LDM policy and table sizes, then the callback order
|
|
/// mirrors the original reset path and stops immediately when table allocation
|
|
/// reports failure.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
struct ZSTDMT_serialResetPlan {
|
|
ldmParams: ZSTDMT_serialResetLdmParameters,
|
|
maxNbSeq: usize,
|
|
hashSize: usize,
|
|
numBuckets: usize,
|
|
bucketLog: c_uint,
|
|
previousBucketLog: c_uint,
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn serial_state_reset_plan(
|
|
projection: ZSTDMT_RustSerialResetProjection,
|
|
) -> ZSTDMT_serialResetPlan {
|
|
let mut ldm_params = ZSTDMT_serialResetLdmParameters {
|
|
enableLdm: projection.enableLdm,
|
|
hashLog: projection.hashLog,
|
|
bucketSizeLog: projection.bucketSizeLog,
|
|
minMatchLength: projection.minMatchLength,
|
|
hashRateLog: projection.hashRateLog,
|
|
windowLog: projection.windowLog,
|
|
};
|
|
if projection.enableLdm == ZSTD_PS_ENABLE {
|
|
let c_params = ZSTD_compressionParameters {
|
|
windowLog: projection.windowLog,
|
|
strategy: projection.strategy,
|
|
..Default::default()
|
|
};
|
|
unsafe { ZSTD_ldm_adjustParameters(&mut ldm_params, &c_params) };
|
|
} else {
|
|
ldm_params = ZSTDMT_serialResetLdmParameters::default();
|
|
}
|
|
|
|
let ldm_enabled = ldm_params.enableLdm == ZSTD_PS_ENABLE;
|
|
let bucket_log = if ldm_enabled {
|
|
ldm_params.hashLog.wrapping_sub(ldm_params.bucketSizeLog)
|
|
} else {
|
|
0
|
|
};
|
|
let previous_bucket_log = if ldm_enabled {
|
|
projection
|
|
.previousHashLog
|
|
.wrapping_sub(projection.previousBucketSizeLog)
|
|
} else {
|
|
0
|
|
};
|
|
let hash_size = if ldm_enabled {
|
|
(1usize.wrapping_shl(ldm_params.hashLog)).wrapping_mul(projection.ldmEntrySize)
|
|
} else {
|
|
0
|
|
};
|
|
let num_buckets = if ldm_enabled {
|
|
1usize.wrapping_shl(bucket_log)
|
|
} else {
|
|
0
|
|
};
|
|
let max_nb_seq = unsafe { ZSTD_ldm_getMaxNbSeq(ldm_params, projection.jobSize) };
|
|
|
|
ZSTDMT_serialResetPlan {
|
|
ldmParams: ldm_params,
|
|
maxNbSeq: max_nb_seq,
|
|
hashSize: hash_size,
|
|
numBuckets: num_buckets,
|
|
bucketLog: bucket_log,
|
|
previousBucketLog: previous_bucket_log,
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateReset(
|
|
projection: *const ZSTDMT_RustSerialResetProjection,
|
|
opaque: *mut c_void,
|
|
set_ldm_params: Option<ZSTDMT_serialResetSetLdmParamsFn>,
|
|
reset_next_job: Option<ZSTDMT_serialResetVoidFn>,
|
|
reset_checksum: Option<ZSTDMT_serialResetVoidFn>,
|
|
set_nb_seq: Option<ZSTDMT_serialResetSetNbSeqFn>,
|
|
reset_window: Option<ZSTDMT_serialResetVoidFn>,
|
|
resize_tables: Option<ZSTDMT_serialResetResizeFn>,
|
|
zero_tables: Option<ZSTDMT_serialResetZeroTablesFn>,
|
|
load_dictionary: Option<ZSTDMT_serialResetVoidFn>,
|
|
copy_window: Option<ZSTDMT_serialResetVoidFn>,
|
|
publish_params: Option<ZSTDMT_serialResetPublishFn>,
|
|
) -> c_int {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return 1;
|
|
};
|
|
let (
|
|
Some(set_ldm_params),
|
|
Some(reset_next_job),
|
|
Some(reset_checksum),
|
|
Some(set_nb_seq),
|
|
Some(reset_window),
|
|
Some(resize_tables),
|
|
Some(zero_tables),
|
|
Some(load_dictionary),
|
|
Some(copy_window),
|
|
Some(publish_params),
|
|
) = (
|
|
set_ldm_params,
|
|
reset_next_job,
|
|
reset_checksum,
|
|
set_nb_seq,
|
|
reset_window,
|
|
resize_tables,
|
|
zero_tables,
|
|
load_dictionary,
|
|
copy_window,
|
|
publish_params,
|
|
)
|
|
else {
|
|
return 1;
|
|
};
|
|
|
|
let plan = unsafe { serial_state_reset_plan(projection) };
|
|
unsafe {
|
|
set_ldm_params(
|
|
opaque,
|
|
plan.ldmParams.enableLdm,
|
|
plan.ldmParams.hashLog,
|
|
plan.ldmParams.bucketSizeLog,
|
|
plan.ldmParams.minMatchLength,
|
|
plan.ldmParams.hashRateLog,
|
|
plan.ldmParams.windowLog,
|
|
);
|
|
reset_next_job(opaque);
|
|
if projection.checksumEnabled != 0 {
|
|
reset_checksum(opaque);
|
|
}
|
|
if plan.ldmParams.enableLdm == ZSTD_PS_ENABLE {
|
|
set_nb_seq(opaque, plan.maxNbSeq);
|
|
reset_window(opaque);
|
|
if resize_tables(
|
|
opaque,
|
|
plan.hashSize,
|
|
plan.numBuckets,
|
|
plan.bucketLog,
|
|
plan.previousBucketLog,
|
|
) != 0
|
|
{
|
|
return 1;
|
|
}
|
|
zero_tables(opaque, plan.hashSize, plan.numBuckets);
|
|
load_dictionary(opaque);
|
|
copy_window(opaque);
|
|
}
|
|
publish_params(opaque, projection.jobSize);
|
|
}
|
|
0
|
|
}
|
|
|
|
/// Run the MT serial-state synchronization initialization order and preserve
|
|
/// the original bitwise error aggregation. C owns each pthread primitive and
|
|
/// the zeroing callback.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateInit(
|
|
opaque: *mut c_void,
|
|
zero_state: Option<ZSTDMT_serialStateVoidFn>,
|
|
init_mutex: Option<ZSTDMT_serialStateInitFn>,
|
|
init_cond: Option<ZSTDMT_serialStateInitFn>,
|
|
init_ldm_mutex: Option<ZSTDMT_serialStateInitFn>,
|
|
init_ldm_cond: Option<ZSTDMT_serialStateInitFn>,
|
|
) -> c_int {
|
|
let (
|
|
Some(zero_state),
|
|
Some(init_mutex),
|
|
Some(init_cond),
|
|
Some(init_ldm_mutex),
|
|
Some(init_ldm_cond),
|
|
) = (
|
|
zero_state,
|
|
init_mutex,
|
|
init_cond,
|
|
init_ldm_mutex,
|
|
init_ldm_cond,
|
|
)
|
|
else {
|
|
return 1;
|
|
};
|
|
|
|
unsafe {
|
|
zero_state(opaque);
|
|
init_mutex(opaque) | init_cond(opaque) | init_ldm_mutex(opaque) | init_ldm_cond(opaque)
|
|
}
|
|
}
|
|
|
|
/// Run the MT serial-state synchronization destruction and table-free order.
|
|
/// The callbacks retain ownership of platform synchronization and allocator
|
|
/// details on the C side.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_serialStateFree(
|
|
opaque: *mut c_void,
|
|
destroy_mutex: Option<ZSTDMT_serialStateVoidFn>,
|
|
destroy_cond: Option<ZSTDMT_serialStateVoidFn>,
|
|
destroy_ldm_mutex: Option<ZSTDMT_serialStateVoidFn>,
|
|
destroy_ldm_cond: Option<ZSTDMT_serialStateVoidFn>,
|
|
free_tables: Option<ZSTDMT_serialStateVoidFn>,
|
|
) {
|
|
let (
|
|
Some(destroy_mutex),
|
|
Some(destroy_cond),
|
|
Some(destroy_ldm_mutex),
|
|
Some(destroy_ldm_cond),
|
|
Some(free_tables),
|
|
) = (
|
|
destroy_mutex,
|
|
destroy_cond,
|
|
destroy_ldm_mutex,
|
|
destroy_ldm_cond,
|
|
free_tables,
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
unsafe {
|
|
destroy_mutex(opaque);
|
|
destroy_cond(opaque);
|
|
destroy_ldm_mutex(opaque);
|
|
destroy_ldm_cond(opaque);
|
|
free_tables(opaque);
|
|
}
|
|
}
|
|
|
|
/// Run the MT context teardown order while C callbacks retain all private
|
|
/// layouts, allocators, worker pools, and synchronization details.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_freeCCtx(state: *const ZSTDMT_RustFreeCCtxState) -> usize {
|
|
if state.is_null() {
|
|
return 0;
|
|
}
|
|
let state = unsafe { &*state };
|
|
let (
|
|
Some(free_factory),
|
|
Some(release_all_job_resources),
|
|
Some(free_jobs),
|
|
Some(free_buffer_pool),
|
|
Some(free_cctx_pool),
|
|
Some(free_seq_pool),
|
|
Some(free_serial_state),
|
|
Some(free_cdict),
|
|
Some(free_round_buffer),
|
|
Some(free_mtctx),
|
|
) = (
|
|
state.free_factory,
|
|
state.release_all_job_resources,
|
|
state.free_jobs,
|
|
state.free_buffer_pool,
|
|
state.free_cctx_pool,
|
|
state.free_seq_pool,
|
|
state.free_serial_state,
|
|
state.free_cdict,
|
|
state.free_round_buffer,
|
|
state.free_mtctx,
|
|
)
|
|
else {
|
|
return 0;
|
|
};
|
|
|
|
unsafe {
|
|
if state.provided_factory == 0 {
|
|
free_factory(state.callback_context);
|
|
}
|
|
release_all_job_resources(state.callback_context);
|
|
free_jobs(state.callback_context);
|
|
free_buffer_pool(state.callback_context);
|
|
free_cctx_pool(state.callback_context);
|
|
free_seq_pool(state.callback_context);
|
|
free_serial_state(state.callback_context);
|
|
free_cdict(state.callback_context);
|
|
if state.round_buffer_present != 0 {
|
|
free_round_buffer(state.callback_context);
|
|
}
|
|
free_mtctx(state.callback_context);
|
|
}
|
|
0
|
|
}
|
|
|
|
/// C ABI entry point for the MT streaming initializer. C owns every
|
|
/// allocation, dictionary handle, synchronization object, and private context
|
|
/// mutation; this wrapper only connects those operations to the Rust policy.
|
|
#[cfg(not(test))]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_initCStream(
|
|
projection: *const ZSTDMT_initCStreamProjection,
|
|
opaque: *mut c_void,
|
|
resize: Option<ZSTDMT_initResizeFn>,
|
|
waitForAllJobs: Option<ZSTDMT_initWaitForAllJobsFn>,
|
|
releaseAllJobResources: Option<ZSTDMT_initReleaseAllJobResourcesFn>,
|
|
applyParameters: Option<ZSTDMT_initApplyParametersFn>,
|
|
prepareDictionary: Option<ZSTDMT_initDictionaryFn>,
|
|
setTargetPrefixSize: Option<ZSTDMT_initSetSizeFn>,
|
|
setTargetSectionSize: Option<ZSTDMT_initSetSizeFn>,
|
|
setRsync: Option<ZSTDMT_initSetRsyncFn>,
|
|
setBufferSize: Option<ZSTDMT_initSetBufferSizeFn>,
|
|
resizeRoundBuffer: Option<ZSTDMT_initResizeRoundBufferFn>,
|
|
resetStream: Option<ZSTDMT_initResetStreamFn>,
|
|
updateDictionary: Option<ZSTDMT_initDictionaryFn>,
|
|
serialReset: Option<ZSTDMT_initSerialResetFn>,
|
|
) -> usize {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
let (
|
|
Some(resize),
|
|
Some(wait_for_all_jobs),
|
|
Some(release_all_job_resources),
|
|
Some(apply_parameters),
|
|
Some(prepare_dictionary),
|
|
Some(set_target_prefix_size),
|
|
Some(set_target_section_size),
|
|
Some(set_rsync),
|
|
Some(set_buffer_size),
|
|
Some(resize_round_buffer),
|
|
Some(reset_stream),
|
|
Some(update_dictionary),
|
|
Some(serial_reset),
|
|
) = (
|
|
resize,
|
|
waitForAllJobs,
|
|
releaseAllJobResources,
|
|
applyParameters,
|
|
prepareDictionary,
|
|
setTargetPrefixSize,
|
|
setTargetSectionSize,
|
|
setRsync,
|
|
setBufferSize,
|
|
resizeRoundBuffer,
|
|
resetStream,
|
|
updateDictionary,
|
|
serialReset,
|
|
)
|
|
else {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
};
|
|
|
|
init_c_stream_with(
|
|
projection,
|
|
|nb_workers| unsafe { resize(opaque, nb_workers) },
|
|
|| unsafe { wait_for_all_jobs(opaque) },
|
|
|| unsafe { release_all_job_resources(opaque) },
|
|
|job_size| unsafe { apply_parameters(opaque, job_size) },
|
|
|| unsafe { prepare_dictionary(opaque) },
|
|
|size| unsafe { set_target_prefix_size(opaque, size) },
|
|
|size| unsafe { set_target_section_size(opaque, size) },
|
|
|hit_mask, prime_power| unsafe { set_rsync(opaque, hit_mask, prime_power) },
|
|
|size| unsafe { set_buffer_size(opaque, size) },
|
|
|capacity| unsafe { resize_round_buffer(opaque, capacity) },
|
|
|| unsafe { reset_stream(opaque) },
|
|
|| unsafe { update_dictionary(opaque) },
|
|
|target_section_size| unsafe { serial_reset(opaque, target_section_size) },
|
|
)
|
|
}
|
|
|
|
#[inline]
|
|
fn invalid_flush_publication(
|
|
output_pos: usize,
|
|
dst_flushed: usize,
|
|
) -> ZSTDMT_flushPublicationResult {
|
|
ZSTDMT_flushPublicationResult {
|
|
status: FLUSH_PUBLICATION_INVALID_BOUNDS,
|
|
toFlush: 0,
|
|
outputPos: output_pos,
|
|
dstFlushed: dst_flushed,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn publish_job_output_with(
|
|
output_dst: *mut c_void,
|
|
output_size: usize,
|
|
output_pos: usize,
|
|
job_dst: *const c_void,
|
|
job_capacity: usize,
|
|
c_size: usize,
|
|
dst_flushed: usize,
|
|
) -> ZSTDMT_flushPublicationResult {
|
|
let Some(output_available) = output_size.checked_sub(output_pos) else {
|
|
return invalid_flush_publication(output_pos, dst_flushed);
|
|
};
|
|
let Some(job_available) = c_size.checked_sub(dst_flushed) else {
|
|
return invalid_flush_publication(output_pos, dst_flushed);
|
|
};
|
|
if c_size > job_capacity {
|
|
return invalid_flush_publication(output_pos, dst_flushed);
|
|
}
|
|
|
|
let to_flush = output_available.min(job_available);
|
|
if to_flush > 0 {
|
|
if output_dst.is_null() || job_dst.is_null() {
|
|
return invalid_flush_publication(output_pos, dst_flushed);
|
|
}
|
|
unsafe {
|
|
ptr::copy_nonoverlapping(
|
|
job_dst.cast::<u8>().add(dst_flushed),
|
|
output_dst.cast::<u8>().add(output_pos),
|
|
to_flush,
|
|
);
|
|
}
|
|
}
|
|
|
|
ZSTDMT_flushPublicationResult {
|
|
status: FLUSH_PUBLICATION_OK,
|
|
toFlush: to_flush,
|
|
outputPos: output_pos + to_flush,
|
|
dstFlushed: dst_flushed + to_flush,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn flush_produced_result(result: usize, output_pos: usize) -> ZSTDMT_flushProducedResult {
|
|
ZSTDMT_flushProducedResult {
|
|
result,
|
|
outputPos: output_pos,
|
|
updateAllJobsCompleted: 0,
|
|
allJobsCompleted: 0,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn flush_produced_with<P, C, U, F, W, R>(
|
|
context: ZSTDMT_flushContextProjection,
|
|
output_dst: *mut c_void,
|
|
output_size: usize,
|
|
output_pos: usize,
|
|
block_to_flush: c_uint,
|
|
end: c_uint,
|
|
mut project_job: P,
|
|
mut add_frame_checksum: C,
|
|
mut update_job: U,
|
|
mut complete_job: F,
|
|
mut wait_for_all_jobs: W,
|
|
mut release_all_job_resources: R,
|
|
) -> ZSTDMT_flushProducedResult
|
|
where
|
|
P: FnMut(c_uint, c_uint) -> ZSTDMT_flushJobProjection,
|
|
C: FnMut(c_uint, &mut ZSTDMT_flushJobProjection),
|
|
U: FnMut(c_uint, usize),
|
|
F: FnMut(c_uint, usize, usize),
|
|
W: FnMut(),
|
|
R: FnMut(),
|
|
{
|
|
let mut output_pos = output_pos;
|
|
let mut done_job_id = context.doneJobID;
|
|
|
|
if done_job_id < context.nextJobID {
|
|
let job_id = done_job_id & context.jobIDMask;
|
|
let mut projection = project_job(job_id, block_to_flush);
|
|
let src_consumed = projection.consumed;
|
|
let src_size = projection.srcSize;
|
|
|
|
if ERR_isError(projection.cSize) {
|
|
wait_for_all_jobs();
|
|
release_all_job_resources();
|
|
return flush_produced_result(projection.cSize, output_pos);
|
|
}
|
|
|
|
/* The checksum is added only once, after the worker has consumed the
|
|
* whole source range. C retains the serial XXH state and updates the
|
|
* projected cSize through this callback. */
|
|
if src_consumed == src_size && projection.frameChecksumNeeded != 0 {
|
|
add_frame_checksum(job_id, &mut projection);
|
|
}
|
|
|
|
if ERR_isError(projection.cSize) {
|
|
wait_for_all_jobs();
|
|
release_all_job_resources();
|
|
return flush_produced_result(projection.cSize, output_pos);
|
|
}
|
|
|
|
let c_size = projection.cSize;
|
|
let mut dst_flushed = projection.dstFlushed;
|
|
if c_size > 0 {
|
|
let publication = unsafe {
|
|
publish_job_output_with(
|
|
output_dst,
|
|
output_size,
|
|
output_pos,
|
|
projection.dstStart,
|
|
projection.dstCapacity,
|
|
c_size,
|
|
dst_flushed,
|
|
)
|
|
};
|
|
if publication.status != FLUSH_PUBLICATION_OK {
|
|
return flush_produced_result(ERROR(ZstdErrorCode::Generic), output_pos);
|
|
}
|
|
|
|
output_pos = publication.outputPos;
|
|
dst_flushed = publication.dstFlushed;
|
|
update_job(job_id, dst_flushed);
|
|
|
|
if src_consumed == src_size && dst_flushed == c_size {
|
|
complete_job(job_id, src_size, c_size);
|
|
done_job_id = done_job_id.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
/* Match the C API's distinction between known pending output and a
|
|
* live worker whose output size is not known yet. */
|
|
if c_size > dst_flushed {
|
|
return flush_produced_result(c_size - dst_flushed, output_pos);
|
|
}
|
|
if src_size > src_consumed {
|
|
return flush_produced_result(1, output_pos);
|
|
}
|
|
}
|
|
|
|
if done_job_id < context.nextJobID {
|
|
return flush_produced_result(1, output_pos);
|
|
}
|
|
if context.jobReady != 0 {
|
|
return flush_produced_result(1, output_pos);
|
|
}
|
|
if context.inBuffFilled > 0 {
|
|
return flush_produced_result(1, output_pos);
|
|
}
|
|
|
|
/* All jobs are entirely flushed. For ZSTD_e_end the caller asks whether
|
|
* the frame itself has ended, rather than whether buffers are empty. */
|
|
ZSTDMT_flushProducedResult {
|
|
result: if end == ZSTD_E_END {
|
|
(context.frameEnded == 0) as usize
|
|
} else {
|
|
0
|
|
},
|
|
outputPos: output_pos,
|
|
updateAllJobsCompleted: 1,
|
|
allJobsCompleted: context.frameEnded,
|
|
}
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
unsafe extern "C" {
|
|
fn ZSTD_compressContinue_public(
|
|
cctx: *mut c_void,
|
|
dst: *mut c_void,
|
|
dst_capacity: usize,
|
|
src: *const c_void,
|
|
src_size: usize,
|
|
) -> usize;
|
|
fn ZSTD_compressEnd_public(
|
|
cctx: *mut c_void,
|
|
dst: *mut c_void,
|
|
dst_capacity: usize,
|
|
src: *const c_void,
|
|
src_size: usize,
|
|
) -> usize;
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn compress_job_chunks_with(
|
|
cctx: *mut c_void,
|
|
src: *const c_void,
|
|
src_size: usize,
|
|
dst: *mut c_void,
|
|
dst_capacity: usize,
|
|
chunk_size: usize,
|
|
last_job: c_uint,
|
|
progress_context: *mut c_void,
|
|
progress_callback: Option<ZSTDMT_chunkProgressFn>,
|
|
compress_continue: ZstdMtCompressFn,
|
|
compress_end: ZstdMtCompressFn,
|
|
) -> ZSTDMT_chunkProcessResult {
|
|
debug_assert!(chunk_size > 0);
|
|
debug_assert!(chunk_size.is_power_of_two());
|
|
|
|
let nb_chunks = src_size.div_ceil(chunk_size);
|
|
let mut input = src.cast::<u8>();
|
|
let mut produced = 0usize;
|
|
|
|
for chunk_number in 1..nb_chunks {
|
|
let c_size = unsafe {
|
|
compress_continue(
|
|
cctx,
|
|
dst.cast::<u8>().wrapping_add(produced).cast(),
|
|
dst_capacity.wrapping_sub(produced),
|
|
input.cast(),
|
|
chunk_size,
|
|
)
|
|
};
|
|
if ERR_isError(c_size) {
|
|
return ZSTDMT_chunkProcessResult {
|
|
error: c_size,
|
|
lastBlockSize: 0,
|
|
};
|
|
}
|
|
input = input.wrapping_add(chunk_size);
|
|
produced = produced.wrapping_add(c_size);
|
|
debug_assert!(produced < dst_capacity);
|
|
|
|
if let Some(callback) = progress_callback {
|
|
unsafe { callback(progress_context, c_size, chunk_size * chunk_number) };
|
|
}
|
|
}
|
|
|
|
if nb_chunks > 0 || last_job != 0 {
|
|
let last_block_size1 = src_size & (chunk_size - 1);
|
|
let last_block_size = if last_block_size1 == 0 && src_size >= chunk_size {
|
|
chunk_size
|
|
} else {
|
|
last_block_size1
|
|
};
|
|
let c_size = unsafe {
|
|
let dst = dst.cast::<u8>().wrapping_add(produced).cast();
|
|
let dst_capacity = dst_capacity.wrapping_sub(produced);
|
|
let compress = if last_job != 0 {
|
|
compress_end
|
|
} else {
|
|
compress_continue
|
|
};
|
|
compress(cctx, dst, dst_capacity, input.cast(), last_block_size)
|
|
};
|
|
if ERR_isError(c_size) {
|
|
return ZSTDMT_chunkProcessResult {
|
|
error: c_size,
|
|
lastBlockSize: 0,
|
|
};
|
|
}
|
|
return ZSTDMT_chunkProcessResult {
|
|
error: 0,
|
|
lastBlockSize: c_size,
|
|
};
|
|
}
|
|
|
|
ZSTDMT_chunkProcessResult {
|
|
error: 0,
|
|
lastBlockSize: 0,
|
|
}
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_compressJobChunks(
|
|
cctx: *mut c_void,
|
|
src: *const c_void,
|
|
src_size: usize,
|
|
dst: *mut c_void,
|
|
dst_capacity: usize,
|
|
chunk_size: usize,
|
|
last_job: c_uint,
|
|
progress_context: *mut c_void,
|
|
progress_callback: Option<ZSTDMT_chunkProgressFn>,
|
|
) -> ZSTDMT_chunkProcessResult {
|
|
unsafe {
|
|
compress_job_chunks_with(
|
|
cctx,
|
|
src,
|
|
src_size,
|
|
dst,
|
|
dst_capacity,
|
|
chunk_size,
|
|
last_job,
|
|
progress_context,
|
|
progress_callback,
|
|
ZSTD_compressContinue_public,
|
|
ZSTD_compressEnd_public,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_publishJobOutput(
|
|
output_dst: *mut c_void,
|
|
output_size: usize,
|
|
output_pos: usize,
|
|
job_dst: *const c_void,
|
|
job_capacity: usize,
|
|
c_size: usize,
|
|
dst_flushed: usize,
|
|
) -> ZSTDMT_flushPublicationResult {
|
|
unsafe {
|
|
publish_job_output_with(
|
|
output_dst,
|
|
output_size,
|
|
output_pos,
|
|
job_dst,
|
|
job_capacity,
|
|
c_size,
|
|
dst_flushed,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_flushProduced(
|
|
context: *const ZSTDMT_flushContextProjection,
|
|
output_dst: *mut c_void,
|
|
output_size: usize,
|
|
output_pos: usize,
|
|
block_to_flush: c_uint,
|
|
end: c_uint,
|
|
opaque: *mut c_void,
|
|
projectJob: Option<ZSTDMT_flushProjectJobFn>,
|
|
addFrameChecksum: Option<ZSTDMT_flushChecksumFn>,
|
|
updateJob: Option<ZSTDMT_flushUpdateJobFn>,
|
|
completeJob: Option<ZSTDMT_flushCompleteJobFn>,
|
|
waitForAllJobs: Option<ZSTDMT_flushWaitForAllJobsFn>,
|
|
releaseAllJobResources: Option<ZSTDMT_flushReleaseAllJobResourcesFn>,
|
|
) -> ZSTDMT_flushProducedResult {
|
|
let Some(context) = (unsafe { context.as_ref() }).copied() else {
|
|
return flush_produced_result(ERROR(ZstdErrorCode::Generic), output_pos);
|
|
};
|
|
let (
|
|
Some(project_job),
|
|
Some(add_frame_checksum),
|
|
Some(update_job),
|
|
Some(complete_job),
|
|
Some(wait_for_all_jobs),
|
|
Some(release_all_job_resources),
|
|
) = (
|
|
projectJob,
|
|
addFrameChecksum,
|
|
updateJob,
|
|
completeJob,
|
|
waitForAllJobs,
|
|
releaseAllJobResources,
|
|
)
|
|
else {
|
|
return flush_produced_result(ERROR(ZstdErrorCode::Generic), output_pos);
|
|
};
|
|
|
|
unsafe {
|
|
flush_produced_with(
|
|
context,
|
|
output_dst,
|
|
output_size,
|
|
output_pos,
|
|
block_to_flush,
|
|
end,
|
|
|job_id, block_to_flush| {
|
|
let mut projection = ZSTDMT_flushJobProjection::default();
|
|
project_job(opaque, job_id, block_to_flush, &mut projection);
|
|
projection
|
|
},
|
|
|job_id, projection| {
|
|
add_frame_checksum(opaque, job_id, projection);
|
|
},
|
|
|job_id, dst_flushed| {
|
|
update_job(opaque, job_id, dst_flushed);
|
|
},
|
|
|job_id, src_size, c_size| {
|
|
complete_job(opaque, job_id, src_size, c_size);
|
|
},
|
|
|| {
|
|
wait_for_all_jobs(opaque);
|
|
},
|
|
|| {
|
|
release_all_job_resources(opaque);
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn compress_stream_result(
|
|
result: usize,
|
|
input_pos: usize,
|
|
output_pos: usize,
|
|
in_buff_filled: usize,
|
|
) -> ZSTDMT_compressStreamResult {
|
|
ZSTDMT_compressStreamResult {
|
|
result,
|
|
inputPos: input_pos,
|
|
outputPos: output_pos,
|
|
inBuffFilled: in_buff_filled,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn publishable_in_buff_filled(buffer_start: *mut c_void, buffer_filled: usize) -> usize {
|
|
if buffer_start.is_null() {
|
|
0
|
|
} else {
|
|
buffer_filled
|
|
}
|
|
}
|
|
|
|
/// Run one outer MT streaming pass while C retains the context and worker
|
|
/// implementation details.
|
|
///
|
|
/// The callbacks are intentionally narrow: input-range acquisition exposes a
|
|
/// C-owned buffer, job creation can post the prepared job, and flushing can
|
|
/// wait on C-owned job synchronization. Rust owns the bounded input copy, the
|
|
/// ordering of those operations, the end-directive adjustments, and the public
|
|
/// progress/return policy.
|
|
#[allow(clippy::too_many_arguments)]
|
|
unsafe fn compress_stream_generic_with<T, J, F, S>(
|
|
context: ZSTDMT_compressStreamContextProjection,
|
|
input: ZSTDMT_streamInputProjection,
|
|
output: ZSTDMT_streamOutputProjection,
|
|
end_op: c_uint,
|
|
mut try_get_input_range: T,
|
|
mut create_job: J,
|
|
mut flush_produced: F,
|
|
mut find_sync_point: S,
|
|
) -> ZSTDMT_compressStreamResult
|
|
where
|
|
T: FnMut(&mut ZSTDMT_streamInputRangeProjection) -> c_int,
|
|
J: FnMut(usize, c_uint) -> ZSTDMT_streamCreateJobResult,
|
|
F: FnMut(*mut c_void, usize, usize, c_uint, c_uint) -> ZSTDMT_streamFlushResult,
|
|
S: FnMut(
|
|
&ZSTDMT_compressStreamContextProjection,
|
|
&ZSTDMT_streamInputProjection,
|
|
&ZSTDMT_streamInputRangeProjection,
|
|
) -> ZSTDMT_syncPointProjection,
|
|
{
|
|
let mut input_pos = input.pos;
|
|
let output_pos = output.pos;
|
|
let initial_in_buff_filled =
|
|
publishable_in_buff_filled(context.inBuffStart, context.inBuffFilled);
|
|
if input_pos > input.size
|
|
|| output_pos > output.size
|
|
|| context.targetSectionSize == 0
|
|
|| context.inBuffFilled > context.inBuffCapacity
|
|
|| (context.inBuffStart.is_null() && context.inBuffFilled != 0)
|
|
{
|
|
return compress_stream_result(
|
|
ERROR(ZstdErrorCode::Generic),
|
|
input_pos,
|
|
output_pos,
|
|
initial_in_buff_filled,
|
|
);
|
|
}
|
|
|
|
if context.frameEnded != 0 && end_op == ZSTD_E_CONTINUE {
|
|
return compress_stream_result(
|
|
ERROR(ZstdErrorCode::StageWrong),
|
|
input_pos,
|
|
output_pos,
|
|
initial_in_buff_filled,
|
|
);
|
|
}
|
|
|
|
let mut end_op = end_op;
|
|
let mut input_range = ZSTDMT_streamInputRangeProjection {
|
|
bufferStart: context.inBuffStart,
|
|
bufferCapacity: context.inBuffCapacity,
|
|
bufferFilled: context.inBuffFilled,
|
|
};
|
|
let mut forward_input_progress = false;
|
|
|
|
/* Fill the current round-buffer section unless a prepared job is waiting
|
|
* for a worker. This is the same ordering as the C implementation: a
|
|
* blocked range acquisition is allowed to fall through to the flush pass.
|
|
*/
|
|
if context.jobReady == 0 && input.size > input_pos {
|
|
if input_range.bufferStart.is_null() {
|
|
debug_assert_eq!(input_range.bufferFilled, 0);
|
|
let _ = try_get_input_range(&mut input_range);
|
|
}
|
|
|
|
if !input_range.bufferStart.is_null() {
|
|
let sync_point = find_sync_point(&context, &input, &input_range);
|
|
if sync_point.flush != 0 && end_op == ZSTD_E_CONTINUE {
|
|
end_op = ZSTD_E_FLUSH;
|
|
}
|
|
|
|
let available = input.size - input_pos;
|
|
let Some(buffer_left) = input_range
|
|
.bufferCapacity
|
|
.checked_sub(input_range.bufferFilled)
|
|
else {
|
|
return compress_stream_result(
|
|
ERROR(ZstdErrorCode::Generic),
|
|
input_pos,
|
|
output_pos,
|
|
publishable_in_buff_filled(input_range.bufferStart, input_range.bufferFilled),
|
|
);
|
|
};
|
|
if sync_point.toLoad > available
|
|
|| sync_point.toLoad > buffer_left
|
|
|| (sync_point.toLoad != 0 && input.src.is_null())
|
|
{
|
|
return compress_stream_result(
|
|
ERROR(ZstdErrorCode::Generic),
|
|
input_pos,
|
|
output_pos,
|
|
publishable_in_buff_filled(input_range.bufferStart, input_range.bufferFilled),
|
|
);
|
|
}
|
|
|
|
if sync_point.toLoad != 0 {
|
|
let source = input
|
|
.src
|
|
.cast::<u8>()
|
|
.wrapping_add(input_pos)
|
|
.cast::<c_void>();
|
|
unsafe {
|
|
ptr::copy_nonoverlapping(
|
|
source.cast::<u8>(),
|
|
input_range
|
|
.bufferStart
|
|
.cast::<u8>()
|
|
.add(input_range.bufferFilled),
|
|
sync_point.toLoad,
|
|
);
|
|
}
|
|
input_pos += sync_point.toLoad;
|
|
input_range.bufferFilled += sync_point.toLoad;
|
|
forward_input_progress = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if input_pos < input.size && end_op == ZSTD_E_END {
|
|
/* The caller cannot end a frame while input remains outside the
|
|
* current job. The current pass must flush this job first. */
|
|
end_op = ZSTD_E_FLUSH;
|
|
}
|
|
|
|
let mut returned_in_buff_filled =
|
|
publishable_in_buff_filled(input_range.bufferStart, input_range.bufferFilled);
|
|
if context.jobReady != 0
|
|
|| input_range.bufferFilled >= context.targetSectionSize
|
|
|| (end_op != ZSTD_E_CONTINUE && input_range.bufferFilled > 0)
|
|
|| (end_op == ZSTD_E_END && context.frameEnded == 0)
|
|
{
|
|
debug_assert!(input_range.bufferFilled <= context.targetSectionSize);
|
|
let create_result = create_job(input_range.bufferFilled, end_op);
|
|
returned_in_buff_filled = if create_result.inBuffActive != 0 {
|
|
input_range.bufferFilled
|
|
} else {
|
|
0
|
|
};
|
|
if ERR_isError(create_result.result) {
|
|
return compress_stream_result(
|
|
create_result.result,
|
|
input_pos,
|
|
output_pos,
|
|
returned_in_buff_filled,
|
|
);
|
|
}
|
|
}
|
|
|
|
let flush_result = flush_produced(
|
|
output.dst,
|
|
output.size,
|
|
output_pos,
|
|
u32::from(!forward_input_progress),
|
|
end_op,
|
|
);
|
|
let remaining_to_flush = flush_result.result;
|
|
let result = if input_pos < input.size {
|
|
remaining_to_flush.max(1)
|
|
} else {
|
|
remaining_to_flush
|
|
};
|
|
compress_stream_result(
|
|
result,
|
|
input_pos,
|
|
flush_result.outputPos,
|
|
returned_in_buff_filled,
|
|
)
|
|
}
|
|
|
|
/// C ABI entry point for the MT outer scheduling/flush policy.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_compressStreamGeneric(
|
|
context: *const ZSTDMT_compressStreamContextProjection,
|
|
input: *const ZSTDMT_streamInputProjection,
|
|
output: *const ZSTDMT_streamOutputProjection,
|
|
end_op: c_uint,
|
|
opaque: *mut c_void,
|
|
tryGetInputRange: Option<ZSTDMT_streamTryGetInputRangeFn>,
|
|
createJob: Option<ZSTDMT_streamCreateJobFn>,
|
|
flushProduced: Option<ZSTDMT_streamFlushProducedFn>,
|
|
) -> ZSTDMT_compressStreamResult {
|
|
let Some(context) = (unsafe { context.as_ref() }).copied() else {
|
|
return compress_stream_result(ERROR(ZstdErrorCode::Generic), 0, 0, 0);
|
|
};
|
|
let Some(input) = (unsafe { input.as_ref() }).copied() else {
|
|
return compress_stream_result(ERROR(ZstdErrorCode::Generic), 0, 0, context.inBuffFilled);
|
|
};
|
|
let Some(output) = (unsafe { output.as_ref() }).copied() else {
|
|
return compress_stream_result(
|
|
ERROR(ZstdErrorCode::Generic),
|
|
input.pos,
|
|
0,
|
|
context.inBuffFilled,
|
|
);
|
|
};
|
|
let (Some(try_get_input_range), Some(create_job), Some(flush_produced)) =
|
|
(tryGetInputRange, createJob, flushProduced)
|
|
else {
|
|
return compress_stream_result(
|
|
ERROR(ZstdErrorCode::Generic),
|
|
input.pos,
|
|
output.pos,
|
|
context.inBuffFilled,
|
|
);
|
|
};
|
|
|
|
unsafe {
|
|
compress_stream_generic_with(
|
|
context,
|
|
input,
|
|
output,
|
|
end_op,
|
|
|projection| try_get_input_range(opaque, projection),
|
|
|src_size, end| create_job(opaque, src_size, end),
|
|
|dst, size, pos, block_to_flush, end| {
|
|
flush_produced(opaque, dst, size, pos, block_to_flush, end)
|
|
},
|
|
|context, input, input_range| {
|
|
let (to_load, flush) = find_synchronization_point(
|
|
input.src,
|
|
input.size,
|
|
input.pos,
|
|
context.targetSectionSize,
|
|
input_range.bufferStart.cast_const(),
|
|
input_range.bufferFilled,
|
|
context.rsyncable,
|
|
context.rsyncPrimePower,
|
|
context.rsyncHitMask,
|
|
);
|
|
ZSTDMT_syncPointProjection {
|
|
toLoad: to_load,
|
|
flush,
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn get_input_data_in_use_with<F>(
|
|
first_job_id: c_uint,
|
|
last_job_id: c_uint,
|
|
job_id_mask: c_uint,
|
|
round_buffer_capacity: usize,
|
|
target_section_size: usize,
|
|
mut project_job: F,
|
|
) -> ZSTDMT_inputRange
|
|
where
|
|
F: FnMut(c_uint) -> ZSTDMT_jobProjection,
|
|
{
|
|
debug_assert!(target_section_size > 0);
|
|
if target_section_size == 0 {
|
|
return ZSTDMT_inputRange::default();
|
|
}
|
|
|
|
/* No job can refer to the first round buffer before this point. */
|
|
let jobs_before_round_buffer_reuse = round_buffer_capacity / target_section_size;
|
|
/* C's usual arithmetic conversions promote `lastJobID` to size_t here. */
|
|
if (last_job_id as usize) < jobs_before_round_buffer_reuse {
|
|
return ZSTDMT_inputRange::default();
|
|
}
|
|
|
|
for job_id in first_job_id..last_job_id {
|
|
let projection = project_job(job_id & job_id_mask);
|
|
if projection.consumed >= projection.srcSize {
|
|
continue;
|
|
}
|
|
|
|
if projection.prefixSize > 0 {
|
|
debug_assert!((projection.prefixStart as usize) <= (projection.srcStart as usize));
|
|
return ZSTDMT_inputRange {
|
|
start: projection.prefixStart,
|
|
size: projection.prefixSize,
|
|
};
|
|
}
|
|
return ZSTDMT_inputRange {
|
|
start: projection.srcStart,
|
|
size: projection.srcSize,
|
|
};
|
|
}
|
|
|
|
ZSTDMT_inputRange::default()
|
|
}
|
|
|
|
/// Find the earliest still-in-use input range in the C-owned job ring.
|
|
///
|
|
/// C supplies each snapshot under its private job mutex. Rust performs the
|
|
/// ring ordering and prefix/source selection without receiving the job table
|
|
/// or any of its private synchronization layout.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_getInputDataInUse(
|
|
firstJobID: c_uint,
|
|
lastJobID: c_uint,
|
|
jobIDMask: c_uint,
|
|
roundBufferCapacity: usize,
|
|
targetSectionSize: usize,
|
|
opaque: *mut c_void,
|
|
projectJob: Option<ZSTDMT_jobProjectionFn>,
|
|
) -> ZSTDMT_inputRange {
|
|
let Some(project_job) = projectJob else {
|
|
return ZSTDMT_inputRange::default();
|
|
};
|
|
get_input_data_in_use_with(
|
|
firstJobID,
|
|
lastJobID,
|
|
jobIDMask,
|
|
roundBufferCapacity,
|
|
targetSectionSize,
|
|
|job_id| {
|
|
let mut projection = ZSTDMT_jobProjection::default();
|
|
unsafe { project_job(opaque, job_id, &mut projection) };
|
|
projection
|
|
},
|
|
)
|
|
}
|
|
|
|
#[inline]
|
|
fn to_flush_now_with<F>(
|
|
done_job_id: c_uint,
|
|
next_job_id: c_uint,
|
|
job_id_mask: c_uint,
|
|
mut project_job: F,
|
|
) -> usize
|
|
where
|
|
F: FnMut(c_uint) -> ZSTDMT_jobProjection,
|
|
{
|
|
if done_job_id == next_job_id {
|
|
return 0;
|
|
}
|
|
|
|
let projection = project_job(done_job_id & job_id_mask);
|
|
let produced = if ERR_isError(projection.cSize) {
|
|
0
|
|
} else {
|
|
projection.cSize
|
|
};
|
|
let flushed = if ERR_isError(projection.cSize) {
|
|
0
|
|
} else {
|
|
projection.dstFlushed
|
|
};
|
|
debug_assert!(flushed <= produced);
|
|
debug_assert!(projection.consumed <= projection.srcSize);
|
|
|
|
let to_flush = produced.wrapping_sub(flushed);
|
|
if to_flush == 0 {
|
|
/* A live job with no output yet must still have input remaining. */
|
|
debug_assert!(projection.consumed < projection.srcSize);
|
|
}
|
|
to_flush
|
|
}
|
|
|
|
/// Return the output already produced by the oldest active C job but not yet
|
|
/// published to the caller's output buffer.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_toFlushNow(
|
|
doneJobID: c_uint,
|
|
nextJobID: c_uint,
|
|
jobIDMask: c_uint,
|
|
opaque: *mut c_void,
|
|
projectJob: Option<ZSTDMT_jobProjectionFn>,
|
|
) -> usize {
|
|
let Some(project_job) = projectJob else {
|
|
return 0;
|
|
};
|
|
to_flush_now_with(doneJobID, nextJobID, jobIDMask, |job_id| {
|
|
let mut projection = ZSTDMT_jobProjection::default();
|
|
unsafe { project_job(opaque, job_id, &mut projection) };
|
|
projection
|
|
})
|
|
}
|
|
|
|
#[inline]
|
|
fn try_get_input_range_with(
|
|
projection: ZSTDMT_tryGetInputRangeProjection,
|
|
) -> ZSTDMT_tryGetInputRangeResult {
|
|
let invalid = || ZSTDMT_tryGetInputRangeResult::default();
|
|
|
|
debug_assert!(projection.roundBufferPos <= projection.roundBufferCapacity);
|
|
debug_assert!(projection.targetSectionSize > 0);
|
|
if projection.roundBufferPos > projection.roundBufferCapacity
|
|
|| projection.targetSectionSize == 0
|
|
|| projection.targetSectionSize > projection.roundBufferCapacity
|
|
{
|
|
return invalid();
|
|
}
|
|
|
|
let space_left = projection.roundBufferCapacity - projection.roundBufferPos;
|
|
let (round_buffer_pos, move_prefix) = if space_left < projection.targetSectionSize {
|
|
/* The C implementation first checks the prefix range at the start of
|
|
* the round buffer before resetting the position there. */
|
|
if is_overlapped(
|
|
projection.roundBufferStart,
|
|
projection.prefixSize,
|
|
projection.inUseStart,
|
|
projection.inUseSize,
|
|
) != 0
|
|
{
|
|
return invalid();
|
|
}
|
|
if projection.prefixSize > projection.roundBufferCapacity - projection.targetSectionSize {
|
|
return invalid();
|
|
}
|
|
(projection.prefixSize, 1)
|
|
} else {
|
|
(projection.roundBufferPos, 0)
|
|
};
|
|
|
|
let buffer_start = projection
|
|
.roundBufferStart
|
|
.cast::<u8>()
|
|
.wrapping_add(round_buffer_pos)
|
|
.cast_mut()
|
|
.cast::<c_void>();
|
|
if move_prefix == 0 {
|
|
/* This is an internal invariant in the original C helper: a live
|
|
* prefix must never be overwritten by the next source section. */
|
|
debug_assert_eq!(
|
|
is_overlapped(
|
|
buffer_start,
|
|
projection.targetSectionSize,
|
|
projection.prefixStart,
|
|
projection.prefixSize,
|
|
),
|
|
0
|
|
);
|
|
}
|
|
if is_overlapped(
|
|
buffer_start,
|
|
projection.targetSectionSize,
|
|
projection.inUseStart,
|
|
projection.inUseSize,
|
|
) != 0
|
|
{
|
|
return invalid();
|
|
}
|
|
|
|
ZSTDMT_tryGetInputRangeResult {
|
|
ready: 1,
|
|
movePrefix: move_prefix,
|
|
bufferStart: buffer_start,
|
|
bufferCapacity: projection.targetSectionSize,
|
|
roundBufferPos: round_buffer_pos,
|
|
}
|
|
}
|
|
|
|
/// Select the next reusable round-buffer section for MT input. The C adapter
|
|
/// retains all synchronization, prefix copying, and private context updates.
|
|
#[cfg(not(test))]
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_tryGetInputRange(
|
|
projection: *const ZSTDMT_tryGetInputRangeProjection,
|
|
) -> ZSTDMT_tryGetInputRangeResult {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return ZSTDMT_tryGetInputRangeResult::default();
|
|
};
|
|
try_get_input_range_with(projection)
|
|
}
|
|
|
|
#[inline]
|
|
fn cycle_log(chain_log: c_uint, strategy: c_int) -> c_uint {
|
|
chain_log.wrapping_sub((strategy >= ZSTD_BTLAZY2) as c_uint)
|
|
}
|
|
|
|
#[inline]
|
|
fn compute_target_job_log(
|
|
window_log: c_uint,
|
|
chain_log: c_uint,
|
|
strategy: c_int,
|
|
enable_ldm: c_int,
|
|
) -> c_uint {
|
|
let job_log = if enable_ldm == ZSTD_PS_ENABLE {
|
|
/* In Long Range Mode, the windowLog is typically oversized.
|
|
* In which case, it's preferable to determine the jobSize
|
|
* based on cycleLog instead. */
|
|
21.max(cycle_log(chain_log, strategy).wrapping_add(3))
|
|
} else {
|
|
20.max(window_log.wrapping_add(2))
|
|
};
|
|
job_log.min(ZSTDMT_JOBLOG_MAX)
|
|
}
|
|
|
|
#[inline]
|
|
fn overlap_log_default(strategy: c_int) -> c_int {
|
|
match strategy {
|
|
ZSTD_BTULTRA2 => 9,
|
|
ZSTD_BTULTRA | ZSTD_BTOPT => 8,
|
|
ZSTD_BTLAZY2 | ZSTD_LAZY2 => 7,
|
|
ZSTD_LAZY | ZSTD_GREEDY | ZSTD_DFAST | ZSTD_FAST => 6,
|
|
_ => 6,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn overlap_log(overlap_log: c_int, strategy: c_int) -> c_int {
|
|
debug_assert!((0..=9).contains(&overlap_log));
|
|
if overlap_log == 0 {
|
|
overlap_log_default(strategy)
|
|
} else {
|
|
overlap_log
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn compute_overlap_size(
|
|
window_log: c_uint,
|
|
chain_log: c_uint,
|
|
strategy: c_int,
|
|
overlap_log_value: c_int,
|
|
enable_ldm: c_int,
|
|
) -> usize {
|
|
let overlap_r_log = 9 - overlap_log(overlap_log_value, strategy);
|
|
let mut overlap_log_value = if overlap_r_log >= 8 {
|
|
0
|
|
} else {
|
|
window_log as c_int - overlap_r_log
|
|
};
|
|
debug_assert!((0..=8).contains(&overlap_r_log));
|
|
if enable_ldm == ZSTD_PS_ENABLE {
|
|
/* In Long Range Mode, the windowLog is typically oversized.
|
|
* In which case, it's preferable to determine the jobSize
|
|
* based on chainLog instead.
|
|
* Then, ovLog becomes a fraction of the jobSize, rather than windowSize */
|
|
let target_job_log = compute_target_job_log(window_log, chain_log, strategy, enable_ldm);
|
|
overlap_log_value = window_log.min(target_job_log.wrapping_sub(2)) as c_int - overlap_r_log;
|
|
}
|
|
debug_assert!(overlap_log_value >= 0);
|
|
debug_assert!(overlap_log_value <= ZSTD_WINDOWLOG_MAX as c_int);
|
|
if overlap_log_value == 0 {
|
|
0
|
|
} else {
|
|
1usize << overlap_log_value as usize
|
|
}
|
|
}
|
|
|
|
/// C ABI for the pure MT target job-log policy.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_computeTargetJobLog(
|
|
windowLog: c_uint,
|
|
chainLog: c_uint,
|
|
strategy: c_int,
|
|
enableLdm: c_int,
|
|
) -> c_uint {
|
|
compute_target_job_log(windowLog, chainLog, strategy, enableLdm)
|
|
}
|
|
|
|
/// C ABI for the MT overlap-log default/selection policy.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_overlapLog(overlapLog: c_int, strategy: c_int) -> c_int {
|
|
overlap_log(overlapLog, strategy)
|
|
}
|
|
|
|
/// C ABI for the pure MT overlap-size policy.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_computeOverlapSize(
|
|
windowLog: c_uint,
|
|
chainLog: c_uint,
|
|
strategy: c_int,
|
|
overlapLog: c_int,
|
|
enableLdm: c_int,
|
|
) -> usize {
|
|
compute_overlap_size(windowLog, chainLog, strategy, overlapLog, enableLdm)
|
|
}
|
|
|
|
#[inline]
|
|
fn frame_progression(
|
|
consumed: u64,
|
|
in_buff_filled: usize,
|
|
produced: u64,
|
|
current_job_id: c_uint,
|
|
) -> ZSTD_frameProgression {
|
|
ZSTD_frameProgression {
|
|
// C's usual arithmetic conversions promote size_t to U64 before wrapping.
|
|
ingested: consumed.wrapping_add(in_buff_filled as u64),
|
|
consumed,
|
|
produced,
|
|
flushed: produced,
|
|
currentJobID: current_job_id,
|
|
nbActiveWorkers: 0,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn frame_progression_add_job(
|
|
mut progression: ZSTD_frameProgression,
|
|
src_size: usize,
|
|
consumed: usize,
|
|
produced: usize,
|
|
flushed: usize,
|
|
) -> ZSTD_frameProgression {
|
|
progression.ingested = progression.ingested.wrapping_add(src_size as u64);
|
|
progression.consumed = progression.consumed.wrapping_add(consumed as u64);
|
|
progression.produced = progression.produced.wrapping_add(produced as u64);
|
|
progression.flushed = progression.flushed.wrapping_add(flushed as u64);
|
|
progression.nbActiveWorkers = progression
|
|
.nbActiveWorkers
|
|
.wrapping_add((consumed < src_size) as c_uint);
|
|
progression
|
|
}
|
|
|
|
#[inline]
|
|
fn frame_progression_with_jobs<F>(
|
|
consumed: u64,
|
|
in_buff_filled: usize,
|
|
produced: u64,
|
|
current_job_id: c_uint,
|
|
done_job_id: c_uint,
|
|
next_job_id: c_uint,
|
|
job_ready: c_uint,
|
|
job_id_mask: c_uint,
|
|
mut project_job: F,
|
|
) -> ZSTD_frameProgression
|
|
where
|
|
F: FnMut(c_uint) -> ZSTDMT_jobProjection,
|
|
{
|
|
let mut progression = frame_progression(consumed, in_buff_filled, produced, current_job_id);
|
|
debug_assert!(job_ready <= 1);
|
|
let last_job_id = next_job_id.wrapping_add(job_ready);
|
|
for job_id in done_job_id..last_job_id {
|
|
let projection = project_job(job_id & job_id_mask);
|
|
let (produced, flushed) = if ERR_isError(projection.cSize) {
|
|
(0, 0)
|
|
} else {
|
|
(projection.cSize, projection.dstFlushed)
|
|
};
|
|
debug_assert!(flushed <= produced);
|
|
progression = frame_progression_add_job(
|
|
progression,
|
|
projection.srcSize,
|
|
projection.consumed,
|
|
produced,
|
|
flushed,
|
|
);
|
|
}
|
|
progression
|
|
}
|
|
|
|
/// Construct the base MT frame progression from C-owned scalar state.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_frameProgression(
|
|
consumed: u64,
|
|
in_buff_filled: usize,
|
|
produced: u64,
|
|
current_job_id: c_uint,
|
|
) -> ZSTD_frameProgression {
|
|
frame_progression(consumed, in_buff_filled, produced, current_job_id)
|
|
}
|
|
|
|
/// Add one C-normalized, mutex-protected job snapshot to MT frame progression.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_frameProgressionAddJob(
|
|
progression: ZSTD_frameProgression,
|
|
src_size: usize,
|
|
consumed: usize,
|
|
produced: usize,
|
|
flushed: usize,
|
|
) -> ZSTD_frameProgression {
|
|
frame_progression_add_job(progression, src_size, consumed, produced, flushed)
|
|
}
|
|
|
|
/// Aggregate the base MT progression with mutex-protected C job snapshots.
|
|
/// Rust owns only the ring scan and normalization policy; C retains the job
|
|
/// descriptor layout and takes each descriptor mutex in `projectJob`.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_frameProgressionWithJobs(
|
|
consumed: u64,
|
|
in_buff_filled: usize,
|
|
produced: u64,
|
|
current_job_id: c_uint,
|
|
done_job_id: c_uint,
|
|
next_job_id: c_uint,
|
|
job_ready: c_uint,
|
|
job_id_mask: c_uint,
|
|
opaque: *mut c_void,
|
|
project_job: Option<ZSTDMT_jobProjectionFn>,
|
|
) -> ZSTD_frameProgression {
|
|
let Some(project_job) = project_job else {
|
|
return frame_progression(consumed, in_buff_filled, produced, current_job_id);
|
|
};
|
|
frame_progression_with_jobs(
|
|
consumed,
|
|
in_buff_filled,
|
|
produced,
|
|
current_job_id,
|
|
done_job_id,
|
|
next_job_id,
|
|
job_ready,
|
|
job_id_mask,
|
|
|job_id| {
|
|
let mut projection = ZSTDMT_jobProjection::default();
|
|
unsafe { project_job(opaque, job_id, &mut projection) };
|
|
projection
|
|
},
|
|
)
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn rolling_hash_append(mut hash: u64, input: *const u8, size: usize) -> u64 {
|
|
for pos in 0..size {
|
|
let byte = u64::from(unsafe { *input.wrapping_add(pos) });
|
|
hash = hash
|
|
.wrapping_mul(PRIME8_BYTES)
|
|
.wrapping_add(byte.wrapping_add(ROLL_HASH_CHAR_OFFSET));
|
|
}
|
|
hash
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn rolling_hash_compute(input: *const u8, size: usize) -> u64 {
|
|
unsafe { rolling_hash_append(0, input, size) }
|
|
}
|
|
|
|
#[inline]
|
|
fn rolling_hash_rotate(hash: u64, to_remove: u8, to_add: u8, prime_power: u64) -> u64 {
|
|
hash.wrapping_sub(
|
|
u64::from(to_remove)
|
|
.wrapping_add(ROLL_HASH_CHAR_OFFSET)
|
|
.wrapping_mul(prime_power),
|
|
)
|
|
.wrapping_mul(PRIME8_BYTES)
|
|
.wrapping_add(u64::from(to_add).wrapping_add(ROLL_HASH_CHAR_OFFSET))
|
|
}
|
|
|
|
#[inline]
|
|
fn rolling_hash_prime_power(length: c_uint) -> u64 {
|
|
let mut base = PRIME8_BYTES;
|
|
let mut exponent = u64::from(length.wrapping_sub(1));
|
|
let mut power: u64 = 1;
|
|
while exponent != 0 {
|
|
if exponent & 1 != 0 {
|
|
power = power.wrapping_mul(base);
|
|
}
|
|
exponent >>= 1;
|
|
base = base.wrapping_mul(base);
|
|
}
|
|
power
|
|
}
|
|
|
|
/// C ABI for the rolling-hash prime power used by rsyncable MT compression.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_rollingHashPrimePower(length: c_uint) -> u64 {
|
|
rolling_hash_prime_power(length)
|
|
}
|
|
|
|
/// Search for a synchronization point without exposing the C MT context or
|
|
/// its private buffer types across the FFI boundary.
|
|
#[inline]
|
|
unsafe fn find_synchronization_point(
|
|
input_src: *const c_void,
|
|
input_size: usize,
|
|
input_pos: usize,
|
|
target_section_size: usize,
|
|
in_buff_start: *const c_void,
|
|
in_buff_filled: usize,
|
|
rsyncable: c_int,
|
|
prime_power: u64,
|
|
hit_mask: u64,
|
|
) -> (usize, c_int) {
|
|
let istart = input_src.cast::<u8>().wrapping_add(input_pos);
|
|
let available = input_size.wrapping_sub(input_pos);
|
|
let mut to_load = available.min(target_section_size.wrapping_sub(in_buff_filled));
|
|
let mut flush = 0;
|
|
|
|
if rsyncable == 0 {
|
|
return (to_load, flush);
|
|
}
|
|
if in_buff_filled.wrapping_add(available) < RSYNC_MIN_BLOCK_SIZE {
|
|
return (to_load, flush);
|
|
}
|
|
if in_buff_filled.wrapping_add(to_load) < RSYNC_LENGTH {
|
|
return (to_load, flush);
|
|
}
|
|
|
|
let (mut pos, prev, mut hash) = if in_buff_filled < RSYNC_MIN_BLOCK_SIZE {
|
|
let pos = RSYNC_MIN_BLOCK_SIZE.wrapping_sub(in_buff_filled);
|
|
if pos >= RSYNC_LENGTH {
|
|
let prev = istart.wrapping_add(pos.wrapping_sub(RSYNC_LENGTH));
|
|
let hash = unsafe { rolling_hash_compute(prev, RSYNC_LENGTH) };
|
|
(pos, prev, hash)
|
|
} else {
|
|
debug_assert!(in_buff_filled >= RSYNC_LENGTH);
|
|
let prev = in_buff_start
|
|
.cast::<u8>()
|
|
.wrapping_add(in_buff_filled.wrapping_sub(RSYNC_LENGTH));
|
|
let hash = unsafe { rolling_hash_compute(prev.wrapping_add(pos), RSYNC_LENGTH - pos) };
|
|
let hash = unsafe { rolling_hash_append(hash, istart, pos) };
|
|
(pos, prev, hash)
|
|
}
|
|
} else {
|
|
let pos = 0;
|
|
let prev = in_buff_start
|
|
.cast::<u8>()
|
|
.wrapping_add(in_buff_filled.wrapping_sub(RSYNC_LENGTH));
|
|
let hash = unsafe { rolling_hash_compute(prev, RSYNC_LENGTH) };
|
|
if (hash & hit_mask) == hit_mask {
|
|
return (0, 1);
|
|
}
|
|
(pos, prev, hash)
|
|
};
|
|
|
|
debug_assert!(
|
|
pos < RSYNC_LENGTH
|
|
|| unsafe {
|
|
rolling_hash_compute(
|
|
istart.wrapping_add(pos.wrapping_sub(RSYNC_LENGTH)),
|
|
RSYNC_LENGTH,
|
|
) == hash
|
|
}
|
|
);
|
|
while pos < to_load {
|
|
let to_remove = if pos < RSYNC_LENGTH {
|
|
unsafe { *prev.wrapping_add(pos) }
|
|
} else {
|
|
unsafe { *istart.wrapping_add(pos.wrapping_sub(RSYNC_LENGTH)) }
|
|
};
|
|
let to_add = unsafe { *istart.wrapping_add(pos) };
|
|
hash = rolling_hash_rotate(hash, to_remove, to_add, prime_power);
|
|
debug_assert!(in_buff_filled.wrapping_add(pos) >= RSYNC_MIN_BLOCK_SIZE);
|
|
if (hash & hit_mask) == hit_mask {
|
|
to_load = pos + 1;
|
|
flush = 1;
|
|
pos += 1;
|
|
break;
|
|
}
|
|
pos += 1;
|
|
}
|
|
debug_assert!(
|
|
pos < RSYNC_LENGTH
|
|
|| unsafe {
|
|
rolling_hash_compute(
|
|
istart.wrapping_add(pos.wrapping_sub(RSYNC_LENGTH)),
|
|
RSYNC_LENGTH,
|
|
) == hash
|
|
}
|
|
);
|
|
(to_load, flush)
|
|
}
|
|
|
|
/// C ABI for the pure MT synchronization-point policy.
|
|
///
|
|
/// The caller retains ownership of the MT context, input position, and
|
|
/// buffer updates. Rust only scans the two byte ranges and writes the two
|
|
/// scalar results.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_findSynchronizationPoint(
|
|
inputSrc: *const c_void,
|
|
inputSize: usize,
|
|
inputPos: usize,
|
|
targetSectionSize: usize,
|
|
inBuffStart: *const c_void,
|
|
inBuffFilled: usize,
|
|
rsyncable: c_int,
|
|
primePower: u64,
|
|
hitMask: u64,
|
|
toLoad: *mut usize,
|
|
flush: *mut c_int,
|
|
) {
|
|
let (computed_to_load, computed_flush) = unsafe {
|
|
find_synchronization_point(
|
|
inputSrc,
|
|
inputSize,
|
|
inputPos,
|
|
targetSectionSize,
|
|
inBuffStart,
|
|
inBuffFilled,
|
|
rsyncable,
|
|
primePower,
|
|
hitMask,
|
|
)
|
|
};
|
|
unsafe {
|
|
*toLoad = computed_to_load;
|
|
*flush = computed_flush;
|
|
}
|
|
}
|
|
|
|
type ZstdAllocFunction = unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void;
|
|
type ZstdFreeFunction = unsafe extern "C" fn(*mut c_void, *mut c_void);
|
|
|
|
/// ABI-compatible representation of `ZSTD_customMem` from `zstd.h`.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZstdCustomMem {
|
|
pub customAlloc: Option<ZstdAllocFunction>,
|
|
pub customFree: Option<ZstdFreeFunction>,
|
|
pub opaque: *mut c_void,
|
|
}
|
|
|
|
/// ABI-compatible buffer returned to the C adapter.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Default)]
|
|
pub struct ZstdMtBuffer {
|
|
pub start: *mut c_void,
|
|
pub capacity: usize,
|
|
}
|
|
|
|
/// ABI-compatible representation of the private C `ZSTD_window_t` used by
|
|
/// the MT LDM overlap callback.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Default)]
|
|
pub struct ZstdMtWindow {
|
|
pub nextSrc: *const u8,
|
|
pub base: *const u8,
|
|
pub dictBase: *const u8,
|
|
pub dictLimit: u32,
|
|
pub lowLimit: u32,
|
|
pub nbOverflowCorrections: u32,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(std::mem::offset_of!(ZstdMtWindow, nextSrc) == 0);
|
|
assert!(std::mem::offset_of!(ZstdMtWindow, base) == size_of::<usize>());
|
|
assert!(std::mem::offset_of!(ZstdMtWindow, dictBase) == 2 * size_of::<usize>());
|
|
assert!(std::mem::offset_of!(ZstdMtWindow, dictLimit) == 3 * size_of::<usize>());
|
|
assert!(size_of::<ZstdMtWindow>() == if size_of::<usize>() == 8 { 40 } else { 24 });
|
|
};
|
|
|
|
/// ABI-compatible representation of `rawSeq` from `zstd_compress_internal.h`.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Default)]
|
|
pub struct ZstdMtRawSeq {
|
|
pub offset: u32,
|
|
pub litLength: u32,
|
|
pub matchLength: u32,
|
|
}
|
|
|
|
/// ABI-compatible representation of `RawSeqStore_t` from
|
|
/// `zstd_compress_internal.h`.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Default)]
|
|
pub struct ZstdMtRawSeqStore {
|
|
pub seq: *mut ZstdMtRawSeq,
|
|
pub pos: usize,
|
|
pub posInSequence: usize,
|
|
pub size: usize,
|
|
pub capacity: usize,
|
|
}
|
|
|
|
#[inline]
|
|
fn buffer_to_seq(buffer: ZstdMtBuffer) -> ZstdMtRawSeqStore {
|
|
ZstdMtRawSeqStore {
|
|
seq: buffer.start.cast::<ZstdMtRawSeq>(),
|
|
pos: 0,
|
|
posInSequence: 0,
|
|
size: 0,
|
|
capacity: buffer.capacity / mem::size_of::<ZstdMtRawSeq>(),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn seq_to_buffer(seq: ZstdMtRawSeqStore) -> ZstdMtBuffer {
|
|
ZstdMtBuffer {
|
|
start: seq.seq.cast::<c_void>(),
|
|
capacity: seq.capacity.wrapping_mul(mem::size_of::<ZstdMtRawSeq>()),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn is_overlapped(
|
|
buffer_start: *const c_void,
|
|
buffer_capacity: usize,
|
|
range_start: *const c_void,
|
|
range_size: usize,
|
|
) -> c_int {
|
|
if buffer_start.is_null() || range_start.is_null() {
|
|
return 0;
|
|
}
|
|
|
|
let buffer_start = buffer_start.cast::<u8>();
|
|
let range_start = range_start.cast::<u8>();
|
|
let buffer_end = buffer_start.wrapping_add(buffer_capacity);
|
|
let range_end = range_start.wrapping_add(range_size);
|
|
|
|
/* Empty ranges cannot overlap. */
|
|
if buffer_start == buffer_end || range_start == range_end {
|
|
return 0;
|
|
}
|
|
|
|
(buffer_start < range_end && range_start < buffer_end) as c_int
|
|
}
|
|
|
|
/// Convert a byte buffer into the raw-sequence store view used by the MT
|
|
/// sequence pool. The capacity is expressed in whole `rawSeq` elements, just
|
|
/// like the original C conversion leaf.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_bufferToSeq(buffer: ZstdMtBuffer) -> ZstdMtRawSeqStore {
|
|
buffer_to_seq(buffer)
|
|
}
|
|
|
|
/// Convert the raw-sequence store view back to a byte buffer for pool APIs.
|
|
/// The returned capacity is measured in bytes and follows C `size_t` wraparound
|
|
/// semantics for the multiplication.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_seqToBuffer(seq: ZstdMtRawSeqStore) -> ZstdMtBuffer {
|
|
seq_to_buffer(seq)
|
|
}
|
|
|
|
/// Return non-zero when two non-empty byte ranges overlap.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_isOverlapped(
|
|
bufferStart: *const c_void,
|
|
bufferCapacity: usize,
|
|
rangeStart: *const c_void,
|
|
rangeSize: usize,
|
|
) -> c_int {
|
|
is_overlapped(bufferStart, bufferCapacity, rangeStart, rangeSize)
|
|
}
|
|
|
|
/// Return non-zero when a buffer overlaps either the external dictionary or
|
|
/// the active prefix represented by a C `ZSTD_window_t`.
|
|
///
|
|
/// The C window stores the two ranges as pointer/index pairs. Passing those
|
|
/// scalar fields separately keeps the private C struct out of the Rust ABI;
|
|
/// the byte-distance arithmetic mirrors the original pointer subtraction.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_doesOverlapWindow(
|
|
bufferStart: *const c_void,
|
|
bufferCapacity: usize,
|
|
nextSrc: *const c_void,
|
|
base: *const c_void,
|
|
dictBase: *const c_void,
|
|
dictLimit: u32,
|
|
lowLimit: u32,
|
|
) -> c_int {
|
|
let ext_dict_start = dictBase.cast::<u8>().wrapping_add(lowLimit as usize);
|
|
let ext_dict_size = dictLimit.wrapping_sub(lowLimit) as usize;
|
|
let prefix_start = base.cast::<u8>().wrapping_add(dictLimit as usize);
|
|
let prefix_size = (nextSrc as usize)
|
|
.wrapping_sub(base as usize)
|
|
.wrapping_sub(dictLimit as usize);
|
|
|
|
(is_overlapped(
|
|
bufferStart,
|
|
bufferCapacity,
|
|
ext_dict_start.cast(),
|
|
ext_dict_size,
|
|
) != 0
|
|
|| is_overlapped(
|
|
bufferStart,
|
|
bufferCapacity,
|
|
prefix_start.cast(),
|
|
prefix_size,
|
|
) != 0) as c_int
|
|
}
|
|
|
|
/// Return non-zero when a buffer overlaps either range in a C window.
|
|
///
|
|
/// This keeps the original by-value internal ABI while the scalar helper
|
|
/// above remains available to projections that cannot expose the private
|
|
/// window layout.
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_doesOverlapWindow(buffer: ZstdMtBuffer, window: ZstdMtWindow) -> c_int {
|
|
ZSTDMT_rust_doesOverlapWindow(
|
|
buffer.start as *const c_void,
|
|
buffer.capacity,
|
|
window.nextSrc.cast(),
|
|
window.base.cast(),
|
|
window.dictBase.cast(),
|
|
window.dictLimit,
|
|
window.lowLimit,
|
|
)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct BufferPoolState {
|
|
buffer_size: usize,
|
|
nb_buffers: usize,
|
|
}
|
|
|
|
/// The object is allocated with the caller's `ZSTD_customMem`; its mutex
|
|
/// protects all mutable pool state and the raw reusable-buffer array.
|
|
pub struct RustBufferPool {
|
|
custom_mem: ZstdCustomMem,
|
|
total_buffers: usize,
|
|
buffers: *mut MaybeUninit<ZstdMtBuffer>,
|
|
state: Mutex<BufferPoolState>,
|
|
}
|
|
|
|
// The raw array is only accessed while `state` is held. The pool itself is
|
|
// passed between C worker threads as an opaque pointer.
|
|
unsafe impl Send for RustBufferPool {}
|
|
unsafe impl Sync for RustBufferPool {}
|
|
|
|
/// The `ZSTD_CCtx *` values are created and destroyed by the existing C API;
|
|
/// Rust owns only the synchronized reusable-pointer pool.
|
|
pub struct RustCCtxPool {
|
|
custom_mem: ZstdCustomMem,
|
|
total_cctx: usize,
|
|
cctxs: *mut MaybeUninit<*mut c_void>,
|
|
state: Mutex<usize>,
|
|
}
|
|
|
|
unsafe impl Send for RustCCtxPool {}
|
|
unsafe impl Sync for RustCCtxPool {}
|
|
|
|
unsafe extern "C" {
|
|
fn ZSTD_createCCtx_advanced(custom_mem: ZstdCustomMem) -> *mut c_void;
|
|
fn ZSTD_freeCCtx(cctx: *mut c_void) -> usize;
|
|
fn ZSTD_sizeof_CCtx(cctx: *const c_void) -> usize;
|
|
}
|
|
|
|
unsafe fn custom_calloc(size: usize, custom_mem: ZstdCustomMem) -> *mut c_void {
|
|
if let Some(alloc) = custom_mem.customAlloc {
|
|
let allocation = unsafe { alloc(custom_mem.opaque, size) };
|
|
if !allocation.is_null() {
|
|
unsafe { ptr::write_bytes(allocation, 0, size) };
|
|
}
|
|
allocation
|
|
} else {
|
|
unsafe { libc::calloc(1, size) }
|
|
}
|
|
}
|
|
|
|
unsafe fn custom_malloc(size: usize, custom_mem: ZstdCustomMem) -> *mut c_void {
|
|
if let Some(alloc) = custom_mem.customAlloc {
|
|
unsafe { alloc(custom_mem.opaque, size) }
|
|
} else {
|
|
unsafe { libc::malloc(size) }
|
|
}
|
|
}
|
|
|
|
unsafe fn custom_free(allocation: *mut c_void, custom_mem: ZstdCustomMem) {
|
|
if allocation.is_null() {
|
|
return;
|
|
}
|
|
if let Some(free) = custom_mem.customFree {
|
|
unsafe { free(custom_mem.opaque, allocation) };
|
|
} else {
|
|
unsafe { libc::free(allocation) };
|
|
}
|
|
}
|
|
|
|
fn checked_array_size<T>(len: usize) -> Option<usize> {
|
|
mem::size_of::<T>().checked_mul(len)
|
|
}
|
|
|
|
fn rounded_job_count(requested: c_uint) -> Option<c_uint> {
|
|
if requested == 0 {
|
|
return None;
|
|
}
|
|
// The original C expression is `1 << (highbit(requested) + 1)`, which
|
|
// deliberately chooses a strictly larger power of two when requested is
|
|
// already a power of two.
|
|
let shift = usize::BITS - (requested as usize).leading_zeros();
|
|
let count = 1usize.checked_shl(shift)?;
|
|
if count > c_uint::MAX as usize {
|
|
return None;
|
|
}
|
|
Some(count as c_uint)
|
|
}
|
|
|
|
unsafe fn create_job_table(
|
|
nb_jobs_ptr: *mut c_uint,
|
|
job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
) -> *mut c_void {
|
|
if nb_jobs_ptr.is_null() || job_size == 0 {
|
|
return ptr::null_mut();
|
|
}
|
|
let Some(nb_jobs) = (unsafe { rounded_job_count(*nb_jobs_ptr) }) else {
|
|
return ptr::null_mut();
|
|
};
|
|
let Some(table_size) = job_size.checked_mul(nb_jobs as usize) else {
|
|
return ptr::null_mut();
|
|
};
|
|
let table = unsafe { custom_calloc(table_size, custom_mem) };
|
|
if table.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
unsafe { *nb_jobs_ptr = nb_jobs };
|
|
table
|
|
}
|
|
|
|
unsafe fn free_job_table_storage(job_table: *mut c_void, custom_mem: ZstdCustomMem) {
|
|
if job_table.is_null() {
|
|
return;
|
|
}
|
|
unsafe { custom_free(job_table, custom_mem) };
|
|
}
|
|
|
|
type JobTableInitSync = unsafe extern "C" fn(*mut c_void, c_uint, usize) -> c_int;
|
|
type JobTableDestroySync = unsafe extern "C" fn(*mut c_void, c_uint, usize);
|
|
|
|
/// Projection for the final MT job-table release. Rust owns the
|
|
/// destroy-before-storage-free order; C retains the descriptor synchronization
|
|
/// callback and custom allocator behind this opaque projection.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct ZSTDMT_RustFreeJobsTableProjection {
|
|
jobTable: *mut c_void,
|
|
nbJobs: c_uint,
|
|
jobSize: usize,
|
|
customMem: ZstdCustomMem,
|
|
destroySync: Option<JobTableDestroySync>,
|
|
}
|
|
|
|
const _: () = {
|
|
assert!(size_of::<JobTableDestroySync>() == size_of::<usize>());
|
|
assert!(offset_of!(ZSTDMT_RustFreeJobsTableProjection, jobTable) == 0);
|
|
assert!(offset_of!(ZSTDMT_RustFreeJobsTableProjection, nbJobs) == size_of::<usize>());
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustFreeJobsTableProjection, jobSize)
|
|
== if size_of::<usize>() == 8 { 16 } else { 8 }
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustFreeJobsTableProjection, customMem)
|
|
== offset_of!(ZSTDMT_RustFreeJobsTableProjection, jobSize) + size_of::<usize>()
|
|
);
|
|
assert!(
|
|
offset_of!(ZSTDMT_RustFreeJobsTableProjection, destroySync)
|
|
== offset_of!(ZSTDMT_RustFreeJobsTableProjection, customMem)
|
|
+ size_of::<ZstdCustomMem>()
|
|
);
|
|
assert!(
|
|
size_of::<ZSTDMT_RustFreeJobsTableProjection>()
|
|
== offset_of!(ZSTDMT_RustFreeJobsTableProjection, destroySync)
|
|
+ size_of::<JobTableDestroySync>()
|
|
);
|
|
};
|
|
|
|
unsafe fn free_job_table_with_sync(
|
|
job_table: *mut c_void,
|
|
nb_jobs: c_uint,
|
|
job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
destroy_sync: JobTableDestroySync,
|
|
) {
|
|
let projection = ZSTDMT_RustFreeJobsTableProjection {
|
|
jobTable: job_table,
|
|
nbJobs: nb_jobs,
|
|
jobSize: job_size,
|
|
customMem: custom_mem,
|
|
destroySync: Some(destroy_sync),
|
|
};
|
|
unsafe { free_job_table_with_sync_projection(projection) };
|
|
}
|
|
|
|
unsafe fn free_job_table_with_sync_projection(projection: ZSTDMT_RustFreeJobsTableProjection) {
|
|
if projection.jobTable.is_null() {
|
|
return;
|
|
}
|
|
let Some(destroy_sync) = projection.destroySync else {
|
|
return;
|
|
};
|
|
unsafe {
|
|
destroy_sync(projection.jobTable, projection.nbJobs, projection.jobSize);
|
|
free_job_table_storage(projection.jobTable, projection.customMem);
|
|
}
|
|
}
|
|
|
|
unsafe fn create_job_table_with_sync(
|
|
nb_jobs_ptr: *mut c_uint,
|
|
job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
init_sync: JobTableInitSync,
|
|
destroy_sync: JobTableDestroySync,
|
|
) -> *mut c_void {
|
|
let job_table = unsafe { create_job_table(nb_jobs_ptr, job_size, custom_mem) };
|
|
if job_table.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
let nb_jobs = unsafe { *nb_jobs_ptr };
|
|
if unsafe { init_sync(job_table, nb_jobs, job_size) } != 0 {
|
|
unsafe { free_job_table_with_sync(job_table, nb_jobs, job_size, custom_mem, destroy_sync) };
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
job_table
|
|
}
|
|
|
|
unsafe fn expand_job_table(
|
|
job_table_ptr: *mut *mut c_void,
|
|
job_id_mask_ptr: *mut c_uint,
|
|
nb_workers: c_uint,
|
|
job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
init_sync: JobTableInitSync,
|
|
destroy_sync: JobTableDestroySync,
|
|
) -> usize {
|
|
if job_table_ptr.is_null() || job_id_mask_ptr.is_null() {
|
|
return ERROR(ZstdErrorCode::Generic);
|
|
}
|
|
|
|
let requested_jobs = nb_workers.wrapping_add(2);
|
|
let current_capacity = unsafe { (*job_id_mask_ptr).wrapping_add(1) };
|
|
if requested_jobs <= current_capacity {
|
|
return 0;
|
|
}
|
|
|
|
let old_job_table = unsafe { *job_table_ptr };
|
|
unsafe {
|
|
free_job_table_with_sync(
|
|
old_job_table,
|
|
current_capacity,
|
|
job_size,
|
|
custom_mem,
|
|
destroy_sync,
|
|
);
|
|
// Match ZSTDMT_expandJobsTable(): after the old table is released, a
|
|
// failed replacement leaves the C context with no table and a zero
|
|
// mask. The C wrapper copies these scalar results back to its private
|
|
// context only after this adapter returns.
|
|
*job_table_ptr = ptr::null_mut();
|
|
*job_id_mask_ptr = 0;
|
|
}
|
|
|
|
let mut nb_jobs = requested_jobs;
|
|
let new_job_table = unsafe {
|
|
create_job_table_with_sync(&mut nb_jobs, job_size, custom_mem, init_sync, destroy_sync)
|
|
};
|
|
if new_job_table.is_null() {
|
|
return ERROR(ZstdErrorCode::MemoryAllocation);
|
|
}
|
|
|
|
debug_assert!(nb_jobs.is_power_of_two());
|
|
unsafe {
|
|
*job_table_ptr = new_job_table;
|
|
*job_id_mask_ptr = nb_jobs.wrapping_sub(1);
|
|
}
|
|
0
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_job_table_create(
|
|
nb_jobs_ptr: *mut c_uint,
|
|
job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
) -> *mut c_void {
|
|
unsafe { create_job_table(nb_jobs_ptr, job_size, custom_mem) }
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_createJobsTable(
|
|
nb_jobs_ptr: *mut c_uint,
|
|
job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
init_sync: JobTableInitSync,
|
|
destroy_sync: JobTableDestroySync,
|
|
) -> *mut c_void {
|
|
unsafe {
|
|
create_job_table_with_sync(nb_jobs_ptr, job_size, custom_mem, init_sync, destroy_sync)
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_job_table_free(
|
|
job_table: *mut c_void,
|
|
_nb_jobs: c_uint,
|
|
_job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
) {
|
|
// The MT C adapter destroys its platform mutexes and condition variables
|
|
// before calling this storage-only release function. Keeping this Rust
|
|
// side free of MT-only C references also preserves single-threaded builds
|
|
// where zstdmt_compress.c is intentionally omitted.
|
|
unsafe { free_job_table_storage(job_table, custom_mem) }
|
|
}
|
|
|
|
/// Release the C-owned synchronization objects before Rust releases the
|
|
/// opaque job-table storage. This is the normal teardown counterpart to the
|
|
/// same order used by failed create/expand transactions.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_freeJobsTable(
|
|
projection: *const ZSTDMT_RustFreeJobsTableProjection,
|
|
) {
|
|
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
|
|
return;
|
|
};
|
|
unsafe { free_job_table_with_sync_projection(projection) };
|
|
}
|
|
|
|
/// Expand the C-owned MT job table when the requested worker count outgrows
|
|
/// its masked capacity. The table storage and custom allocator calls stay in
|
|
/// Rust; C supplies callbacks for the private descriptor synchronization
|
|
/// lifecycle.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_expandJobsTable(
|
|
job_table_ptr: *mut *mut c_void,
|
|
job_id_mask_ptr: *mut c_uint,
|
|
nb_workers: c_uint,
|
|
job_size: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
init_sync: JobTableInitSync,
|
|
destroy_sync: JobTableDestroySync,
|
|
) -> usize {
|
|
unsafe {
|
|
expand_job_table(
|
|
job_table_ptr,
|
|
job_id_mask_ptr,
|
|
nb_workers,
|
|
job_size,
|
|
custom_mem,
|
|
init_sync,
|
|
destroy_sync,
|
|
)
|
|
}
|
|
}
|
|
|
|
unsafe fn create_buffer_pool(
|
|
max_nb_buffers: usize,
|
|
custom_mem: ZstdCustomMem,
|
|
) -> *mut RustBufferPool {
|
|
if max_nb_buffers == 0 {
|
|
return ptr::null_mut();
|
|
}
|
|
let Some(buffer_bytes) = checked_array_size::<MaybeUninit<ZstdMtBuffer>>(max_nb_buffers) else {
|
|
return ptr::null_mut();
|
|
};
|
|
|
|
let pool = unsafe { custom_calloc(mem::size_of::<RustBufferPool>(), custom_mem) }
|
|
.cast::<RustBufferPool>();
|
|
if pool.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
let buffers =
|
|
unsafe { custom_calloc(buffer_bytes, custom_mem) }.cast::<MaybeUninit<ZstdMtBuffer>>();
|
|
if buffers.is_null() {
|
|
unsafe { custom_free(pool.cast(), custom_mem) };
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
unsafe {
|
|
pool.write(RustBufferPool {
|
|
custom_mem,
|
|
total_buffers: max_nb_buffers,
|
|
buffers,
|
|
state: Mutex::new(BufferPoolState {
|
|
buffer_size: 64 << 10,
|
|
nb_buffers: 0,
|
|
}),
|
|
});
|
|
}
|
|
pool
|
|
}
|
|
|
|
unsafe fn destroy_buffer_pool(pool: *mut RustBufferPool) {
|
|
if pool.is_null() {
|
|
return;
|
|
}
|
|
let custom_mem = unsafe { (*pool).custom_mem };
|
|
let buffers = unsafe { (*pool).buffers };
|
|
let total_buffers = unsafe { (*pool).total_buffers };
|
|
for index in 0..total_buffers {
|
|
let buffer = unsafe { buffers.add(index).read().assume_init() };
|
|
unsafe { custom_free(buffer.start, custom_mem) };
|
|
}
|
|
unsafe { ptr::drop_in_place(pool) };
|
|
unsafe {
|
|
custom_free(buffers.cast(), custom_mem);
|
|
custom_free(pool.cast(), custom_mem);
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_create(
|
|
max_nb_buffers: c_uint,
|
|
custom_mem: ZstdCustomMem,
|
|
) -> *mut RustBufferPool {
|
|
unsafe { create_buffer_pool(max_nb_buffers as usize, custom_mem) }
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_free(pool: *mut RustBufferPool) {
|
|
unsafe { destroy_buffer_pool(pool) }
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_sizeof(pool: *const RustBufferPool) -> usize {
|
|
if pool.is_null() {
|
|
return 0;
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let _state = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
let mut total_buffer_size = 0usize;
|
|
for index in 0..pool_ref.total_buffers {
|
|
let buffer = unsafe { pool_ref.buffers.add(index).read().assume_init() };
|
|
total_buffer_size = total_buffer_size.saturating_add(buffer.capacity);
|
|
}
|
|
mem::size_of::<RustBufferPool>()
|
|
.saturating_add(
|
|
pool_ref
|
|
.total_buffers
|
|
.saturating_mul(mem::size_of::<MaybeUninit<ZstdMtBuffer>>()),
|
|
)
|
|
.saturating_add(total_buffer_size)
|
|
}
|
|
|
|
#[inline]
|
|
fn sizeof_buffer_pool(wrapper_size: usize, rust_pool_size: usize) -> usize {
|
|
wrapper_size.wrapping_add(rust_pool_size)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_sizeofBufferPool(wrapperSize: usize, rustPoolSize: usize) -> usize {
|
|
sizeof_buffer_pool(wrapperSize, rustPoolSize)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_set_size(
|
|
pool: *mut RustBufferPool,
|
|
buffer_size: usize,
|
|
) {
|
|
if pool.is_null() {
|
|
return;
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let mut state = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
state.buffer_size = buffer_size;
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_expand(
|
|
pool: *mut RustBufferPool,
|
|
max_nb_buffers: c_uint,
|
|
) -> *mut RustBufferPool {
|
|
if pool.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
let max_nb_buffers = max_nb_buffers as usize;
|
|
let pool_ref = unsafe { &*pool };
|
|
let (total_buffers, buffer_size, custom_mem) = {
|
|
let state = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
(
|
|
pool_ref.total_buffers,
|
|
state.buffer_size,
|
|
pool_ref.custom_mem,
|
|
)
|
|
};
|
|
if total_buffers >= max_nb_buffers {
|
|
return pool;
|
|
}
|
|
|
|
// This matches the original resize contract: the old pool is consumed
|
|
// before creating the larger replacement.
|
|
unsafe { destroy_buffer_pool(pool) };
|
|
let replacement = unsafe { create_buffer_pool(max_nb_buffers, custom_mem) };
|
|
if !replacement.is_null() {
|
|
unsafe { ZSTDMT_rust_buffer_pool_set_size(replacement, buffer_size) };
|
|
}
|
|
replacement
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_get(pool: *mut RustBufferPool) -> ZstdMtBuffer {
|
|
if pool.is_null() {
|
|
return ZstdMtBuffer::default();
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let (buffer_size, reusable) = {
|
|
let mut state = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
if state.buffer_size == 0 {
|
|
return ZstdMtBuffer::default();
|
|
}
|
|
if state.nb_buffers == 0 {
|
|
(state.buffer_size, ZstdMtBuffer::default())
|
|
} else {
|
|
state.nb_buffers -= 1;
|
|
let index = state.nb_buffers;
|
|
let buffer = unsafe { pool_ref.buffers.add(index).read().assume_init() };
|
|
unsafe {
|
|
pool_ref
|
|
.buffers
|
|
.add(index)
|
|
.write(MaybeUninit::new(ZstdMtBuffer::default()));
|
|
}
|
|
(state.buffer_size, buffer)
|
|
}
|
|
};
|
|
|
|
if !reusable.start.is_null()
|
|
&& reusable.capacity >= buffer_size
|
|
&& (reusable.capacity >> 3) <= buffer_size
|
|
{
|
|
return reusable;
|
|
}
|
|
if !reusable.start.is_null() {
|
|
unsafe { custom_free(reusable.start, pool_ref.custom_mem) };
|
|
}
|
|
|
|
let start = unsafe { custom_malloc(buffer_size, pool_ref.custom_mem) };
|
|
ZstdMtBuffer {
|
|
start,
|
|
capacity: if start.is_null() { 0 } else { buffer_size },
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_release(
|
|
pool: *mut RustBufferPool,
|
|
buffer: ZstdMtBuffer,
|
|
) {
|
|
if pool.is_null() || buffer.start.is_null() {
|
|
return;
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let mut state = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
if state.nb_buffers < pool_ref.total_buffers {
|
|
let index = state.nb_buffers;
|
|
unsafe {
|
|
pool_ref.buffers.add(index).write(MaybeUninit::new(buffer));
|
|
}
|
|
state.nb_buffers += 1;
|
|
return;
|
|
}
|
|
drop(state);
|
|
unsafe { custom_free(buffer.start, pool_ref.custom_mem) };
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_buffer_pool_resize(
|
|
pool: *mut RustBufferPool,
|
|
buffer: ZstdMtBuffer,
|
|
) -> ZstdMtBuffer {
|
|
if pool.is_null() || buffer.start.is_null() {
|
|
return buffer;
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let buffer_size = {
|
|
let state = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
state.buffer_size
|
|
};
|
|
if buffer.capacity >= buffer_size {
|
|
return buffer;
|
|
}
|
|
let start = unsafe { custom_malloc(buffer_size, pool_ref.custom_mem) };
|
|
if start.is_null() {
|
|
return buffer;
|
|
}
|
|
unsafe {
|
|
ptr::copy_nonoverlapping(
|
|
buffer.start.cast::<u8>(),
|
|
start.cast::<u8>(),
|
|
buffer.capacity,
|
|
);
|
|
custom_free(buffer.start, pool_ref.custom_mem);
|
|
}
|
|
ZstdMtBuffer {
|
|
start,
|
|
capacity: buffer_size,
|
|
}
|
|
}
|
|
|
|
unsafe fn create_cctx_pool(nb_workers: usize, custom_mem: ZstdCustomMem) -> *mut RustCCtxPool {
|
|
if nb_workers == 0 {
|
|
return ptr::null_mut();
|
|
}
|
|
let Some(cctx_bytes) = checked_array_size::<MaybeUninit<*mut c_void>>(nb_workers) else {
|
|
return ptr::null_mut();
|
|
};
|
|
let pool =
|
|
unsafe { custom_calloc(mem::size_of::<RustCCtxPool>(), custom_mem) }.cast::<RustCCtxPool>();
|
|
if pool.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
let cctxs = unsafe { custom_calloc(cctx_bytes, custom_mem) }.cast::<MaybeUninit<*mut c_void>>();
|
|
if cctxs.is_null() {
|
|
unsafe { custom_free(pool.cast(), custom_mem) };
|
|
return ptr::null_mut();
|
|
}
|
|
let first = unsafe { ZSTD_createCCtx_advanced(custom_mem) };
|
|
if first.is_null() {
|
|
unsafe {
|
|
custom_free(cctxs.cast(), custom_mem);
|
|
custom_free(pool.cast(), custom_mem);
|
|
}
|
|
return ptr::null_mut();
|
|
}
|
|
unsafe { cctxs.write(MaybeUninit::new(first)) };
|
|
unsafe {
|
|
pool.write(RustCCtxPool {
|
|
custom_mem,
|
|
total_cctx: nb_workers,
|
|
cctxs,
|
|
state: Mutex::new(1),
|
|
});
|
|
}
|
|
pool
|
|
}
|
|
|
|
unsafe fn destroy_cctx_pool(pool: *mut RustCCtxPool) {
|
|
if pool.is_null() {
|
|
return;
|
|
}
|
|
let custom_mem = unsafe { (*pool).custom_mem };
|
|
let cctxs = unsafe { (*pool).cctxs };
|
|
let total_cctx = unsafe { (*pool).total_cctx };
|
|
for index in 0..total_cctx {
|
|
let cctx = unsafe { cctxs.add(index).read().assume_init() };
|
|
unsafe { ZSTD_freeCCtx(cctx) };
|
|
}
|
|
unsafe { ptr::drop_in_place(pool) };
|
|
unsafe {
|
|
custom_free(cctxs.cast(), custom_mem);
|
|
custom_free(pool.cast(), custom_mem);
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_create(
|
|
nb_workers: c_uint,
|
|
custom_mem: ZstdCustomMem,
|
|
) -> *mut RustCCtxPool {
|
|
unsafe { create_cctx_pool(nb_workers as usize, custom_mem) }
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_free(pool: *mut RustCCtxPool) {
|
|
unsafe { destroy_cctx_pool(pool) }
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_sizeof(pool: *const RustCCtxPool) -> usize {
|
|
if pool.is_null() {
|
|
return 0;
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let _state = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
let mut total_cctx_size = 0usize;
|
|
for index in 0..pool_ref.total_cctx {
|
|
let cctx = unsafe { pool_ref.cctxs.add(index).read().assume_init() };
|
|
total_cctx_size =
|
|
total_cctx_size.saturating_add(unsafe { ZSTD_sizeof_CCtx(cctx.cast_const()) });
|
|
}
|
|
mem::size_of::<RustCCtxPool>()
|
|
.saturating_add(
|
|
pool_ref
|
|
.total_cctx
|
|
.saturating_mul(mem::size_of::<MaybeUninit<*mut c_void>>()),
|
|
)
|
|
.saturating_add(total_cctx_size)
|
|
}
|
|
|
|
#[inline]
|
|
fn sizeof_cctx_pool(wrapper_size: usize, rust_pool_size: usize) -> usize {
|
|
wrapper_size.wrapping_add(rust_pool_size)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C" fn ZSTDMT_rust_sizeofCCtxPool(wrapperSize: usize, rustPoolSize: usize) -> usize {
|
|
sizeof_cctx_pool(wrapperSize, rustPoolSize)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_expand(
|
|
pool: *mut RustCCtxPool,
|
|
nb_workers: c_uint,
|
|
) -> *mut RustCCtxPool {
|
|
if pool.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
let nb_workers = nb_workers as usize;
|
|
let pool_ref = unsafe { &*pool };
|
|
if pool_ref.total_cctx >= nb_workers {
|
|
return pool;
|
|
}
|
|
let custom_mem = pool_ref.custom_mem;
|
|
unsafe { destroy_cctx_pool(pool) };
|
|
unsafe { create_cctx_pool(nb_workers, custom_mem) }
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_get(pool: *mut RustCCtxPool) -> *mut c_void {
|
|
if pool.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let cctx = {
|
|
let mut available = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
if *available == 0 {
|
|
ptr::null_mut()
|
|
} else {
|
|
*available -= 1;
|
|
let index = *available;
|
|
unsafe { pool_ref.cctxs.add(index).read().assume_init() }
|
|
}
|
|
};
|
|
if !cctx.is_null() {
|
|
cctx
|
|
} else {
|
|
unsafe { ZSTD_createCCtx_advanced(pool_ref.custom_mem) }
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_release(pool: *mut RustCCtxPool, cctx: *mut c_void) {
|
|
if pool.is_null() || cctx.is_null() {
|
|
return;
|
|
}
|
|
let pool_ref = unsafe { &*pool };
|
|
let mut available = pool_ref
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(|error| error.into_inner());
|
|
if *available < pool_ref.total_cctx {
|
|
let index = *available;
|
|
unsafe {
|
|
pool_ref.cctxs.add(index).write(MaybeUninit::new(cctx));
|
|
}
|
|
*available += 1;
|
|
return;
|
|
}
|
|
drop(available);
|
|
unsafe { ZSTD_freeCCtx(cctx) };
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
use std::{cell::RefCell, rc::Rc};
|
|
|
|
const DEFAULT_MEM: ZstdCustomMem = ZstdCustomMem {
|
|
customAlloc: None,
|
|
customFree: None,
|
|
opaque: ptr::null_mut(),
|
|
};
|
|
|
|
static JOB_TABLE_INIT_CALLS: AtomicUsize = AtomicUsize::new(0);
|
|
static JOB_TABLE_DESTROY_CALLS: AtomicUsize = AtomicUsize::new(0);
|
|
static JOB_TABLE_FAIL_INIT: AtomicBool = AtomicBool::new(false);
|
|
static JOB_TABLE_TEST_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
#[derive(Default)]
|
|
struct FreeCCtxTestContext {
|
|
events: Vec<&'static str>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct CreateCCtxTestContext {
|
|
events: Vec<&'static str>,
|
|
workers: c_uint,
|
|
jobs: c_uint,
|
|
factory_ok: bool,
|
|
resource_calls: usize,
|
|
}
|
|
|
|
fn record_create_cctx_event(context: *mut c_void, event: &'static str) {
|
|
unsafe {
|
|
(*context.cast::<CreateCCtxTestContext>())
|
|
.events
|
|
.push(event);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_allocate(
|
|
context: *mut c_void,
|
|
_size: usize,
|
|
) -> *mut c_void {
|
|
record_create_cctx_event(context, "allocate");
|
|
ptr::dangling_mut::<c_void>()
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_set_workers(
|
|
context: *mut c_void,
|
|
workers: c_uint,
|
|
) -> usize {
|
|
record_create_cctx_event(context, "workers");
|
|
unsafe { (*context.cast::<CreateCCtxTestContext>()).workers = workers };
|
|
0
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_initial_state(context: *mut c_void) {
|
|
record_create_cctx_event(context, "initial-state");
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_factory(
|
|
context: *mut c_void,
|
|
_workers: c_uint,
|
|
) -> *mut c_void {
|
|
record_create_cctx_event(context, "factory");
|
|
let context = unsafe { &*context.cast::<CreateCCtxTestContext>() };
|
|
if context.factory_ok {
|
|
ptr::dangling_mut::<c_void>()
|
|
} else {
|
|
ptr::null_mut()
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_jobs(
|
|
context: *mut c_void,
|
|
jobs: *mut c_uint,
|
|
) -> *mut c_void {
|
|
record_create_cctx_event(context, "jobs");
|
|
unsafe {
|
|
(*context.cast::<CreateCCtxTestContext>()).jobs = 8;
|
|
*jobs = 8;
|
|
}
|
|
ptr::dangling_mut::<c_void>()
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_resource(
|
|
context: *mut c_void,
|
|
_workers: c_uint,
|
|
) -> *mut c_void {
|
|
let event = unsafe {
|
|
let state = &mut *context.cast::<CreateCCtxTestContext>();
|
|
let event = match state.resource_calls {
|
|
0 => "buffer-pool",
|
|
1 => "cctx-pool",
|
|
_ => "seq-pool",
|
|
};
|
|
state.resource_calls += 1;
|
|
event
|
|
};
|
|
record_create_cctx_event(context, event);
|
|
ptr::dangling_mut::<c_void>()
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_serial_init(context: *mut c_void) -> c_int {
|
|
record_create_cctx_event(context, "serial");
|
|
0
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_free(context: *mut c_void) {
|
|
record_create_cctx_event(context, "free");
|
|
}
|
|
|
|
fn create_cctx_test_projection(
|
|
context: &mut CreateCCtxTestContext,
|
|
custom_mem: ZstdCustomMem,
|
|
) -> ZSTDMT_RustCreateCCtxProjection {
|
|
ZSTDMT_RustCreateCCtxProjection {
|
|
callback_context: context as *mut CreateCCtxTestContext as *mut c_void,
|
|
requested_nb_workers: 99,
|
|
max_nb_workers: 4,
|
|
context_size: 128,
|
|
custom_mem,
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ResizeTestContext {
|
|
events: Vec<&'static str>,
|
|
resource_calls: usize,
|
|
fail: Option<&'static str>,
|
|
}
|
|
|
|
fn record_resize_event(context: *mut c_void, event: &'static str) -> bool {
|
|
let state = unsafe { &mut *context.cast::<ResizeTestContext>() };
|
|
state.events.push(event);
|
|
state.fail == Some(event)
|
|
}
|
|
|
|
unsafe extern "C" fn resize_test_step(context: *mut c_void, _workers: c_uint) -> usize {
|
|
let state = unsafe { &*context.cast::<ResizeTestContext>() };
|
|
let event = match state.events.len() {
|
|
0 => "factory",
|
|
_ => "jobs",
|
|
};
|
|
if record_resize_event(context, event) {
|
|
ERROR(ZstdErrorCode::MemoryAllocation)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn resize_test_resource(
|
|
context: *mut c_void,
|
|
_workers: c_uint,
|
|
) -> *mut c_void {
|
|
let state = unsafe { &mut *context.cast::<ResizeTestContext>() };
|
|
let event = match state.resource_calls {
|
|
0 => "buffer-pool",
|
|
1 => "cctx-pool",
|
|
_ => "seq-pool",
|
|
};
|
|
state.resource_calls += 1;
|
|
if record_resize_event(context, event) {
|
|
ptr::null_mut()
|
|
} else {
|
|
ptr::dangling_mut::<c_void>()
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn resize_test_set_workers(context: *mut c_void, _workers: c_uint) -> usize {
|
|
assert!(!record_resize_event(context, "set-workers"));
|
|
0
|
|
}
|
|
|
|
fn resize_test_projection(
|
|
context: &mut ResizeTestContext,
|
|
workers: c_uint,
|
|
) -> ZSTDMT_RustResizeProjection {
|
|
ZSTDMT_RustResizeProjection {
|
|
callback_context: context as *mut ResizeTestContext as *mut c_void,
|
|
nb_workers: workers,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resize_preserves_resource_order_and_publishes_workers_last() {
|
|
let mut context = ResizeTestContext::default();
|
|
let projection = resize_test_projection(&mut context, 6);
|
|
let result = unsafe {
|
|
ZSTDMT_rust_resize(
|
|
&projection,
|
|
Some(resize_test_step),
|
|
Some(resize_test_step),
|
|
Some(resize_test_resource),
|
|
Some(resize_test_resource),
|
|
Some(resize_test_resource),
|
|
Some(resize_test_set_workers),
|
|
)
|
|
};
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(
|
|
context.events,
|
|
[
|
|
"factory",
|
|
"jobs",
|
|
"buffer-pool",
|
|
"cctx-pool",
|
|
"seq-pool",
|
|
"set-workers",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resize_stops_after_resource_failure_without_publishing_workers() {
|
|
let mut context = ResizeTestContext {
|
|
fail: Some("cctx-pool"),
|
|
..ResizeTestContext::default()
|
|
};
|
|
let projection = resize_test_projection(&mut context, 3);
|
|
let result = unsafe {
|
|
ZSTDMT_rust_resize(
|
|
&projection,
|
|
Some(resize_test_step),
|
|
Some(resize_test_step),
|
|
Some(resize_test_resource),
|
|
Some(resize_test_resource),
|
|
Some(resize_test_resource),
|
|
Some(resize_test_set_workers),
|
|
)
|
|
};
|
|
|
|
assert_eq!(result, ERROR(ZstdErrorCode::MemoryAllocation));
|
|
assert_eq!(
|
|
context.events,
|
|
["factory", "jobs", "buffer-pool", "cctx-pool"]
|
|
);
|
|
}
|
|
|
|
fn record_free_cctx_event(context: *mut c_void, event: &'static str) {
|
|
unsafe {
|
|
(*context.cast::<FreeCCtxTestContext>()).events.push(event);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_factory(context: *mut c_void) {
|
|
record_free_cctx_event(context, "factory");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_release_jobs(context: *mut c_void) {
|
|
record_free_cctx_event(context, "release-jobs");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_jobs(context: *mut c_void) {
|
|
record_free_cctx_event(context, "jobs");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_buffer_pool(context: *mut c_void) {
|
|
record_free_cctx_event(context, "buffer-pool");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_cctx_pool(context: *mut c_void) {
|
|
record_free_cctx_event(context, "cctx-pool");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_seq_pool(context: *mut c_void) {
|
|
record_free_cctx_event(context, "seq-pool");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_serial_state(context: *mut c_void) {
|
|
record_free_cctx_event(context, "serial-state");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_cdict(context: *mut c_void) {
|
|
record_free_cctx_event(context, "cdict");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_round_buffer(context: *mut c_void) {
|
|
record_free_cctx_event(context, "round-buffer");
|
|
}
|
|
|
|
unsafe extern "C" fn free_cctx_test_mtctx(context: *mut c_void) {
|
|
record_free_cctx_event(context, "mtctx");
|
|
}
|
|
|
|
fn free_cctx_test_state(
|
|
context: &mut FreeCCtxTestContext,
|
|
provided_factory: usize,
|
|
round_buffer_present: usize,
|
|
) -> ZSTDMT_RustFreeCCtxState {
|
|
ZSTDMT_RustFreeCCtxState {
|
|
callback_context: context as *mut FreeCCtxTestContext as *mut c_void,
|
|
provided_factory,
|
|
round_buffer_present,
|
|
free_factory: Some(free_cctx_test_factory),
|
|
release_all_job_resources: Some(free_cctx_test_release_jobs),
|
|
free_jobs: Some(free_cctx_test_jobs),
|
|
free_buffer_pool: Some(free_cctx_test_buffer_pool),
|
|
free_cctx_pool: Some(free_cctx_test_cctx_pool),
|
|
free_seq_pool: Some(free_cctx_test_seq_pool),
|
|
free_serial_state: Some(free_cctx_test_serial_state),
|
|
free_cdict: Some(free_cctx_test_cdict),
|
|
free_round_buffer: Some(free_cctx_test_round_buffer),
|
|
free_mtctx: Some(free_cctx_test_mtctx),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn free_cctx_null_state_returns_zero_without_callbacks() {
|
|
assert_eq!(unsafe { ZSTDMT_rust_freeCCtx(ptr::null()) }, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn create_cctx_preserves_resource_order_and_clamps_workers() {
|
|
let mut context = CreateCCtxTestContext {
|
|
factory_ok: true,
|
|
..CreateCCtxTestContext::default()
|
|
};
|
|
let projection = create_cctx_test_projection(&mut context, DEFAULT_MEM);
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_createCCtx(
|
|
&projection,
|
|
Some(create_cctx_test_allocate),
|
|
Some(create_cctx_test_set_workers),
|
|
Some(create_cctx_test_initial_state),
|
|
Some(create_cctx_test_factory),
|
|
Some(create_cctx_test_jobs),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_serial_init),
|
|
Some(create_cctx_test_free),
|
|
)
|
|
};
|
|
|
|
assert_eq!(result, ptr::dangling_mut::<c_void>());
|
|
assert_eq!(context.workers, 4);
|
|
assert_eq!(context.jobs, 8);
|
|
assert_eq!(
|
|
context.events,
|
|
vec![
|
|
"allocate",
|
|
"workers",
|
|
"initial-state",
|
|
"factory",
|
|
"jobs",
|
|
"buffer-pool",
|
|
"cctx-pool",
|
|
"seq-pool",
|
|
"serial",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_cctx_runs_remaining_callbacks_before_failure_cleanup() {
|
|
let mut context = CreateCCtxTestContext::default();
|
|
let projection = create_cctx_test_projection(&mut context, DEFAULT_MEM);
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_createCCtx(
|
|
&projection,
|
|
Some(create_cctx_test_allocate),
|
|
Some(create_cctx_test_set_workers),
|
|
Some(create_cctx_test_initial_state),
|
|
Some(create_cctx_test_factory),
|
|
Some(create_cctx_test_jobs),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_serial_init),
|
|
Some(create_cctx_test_free),
|
|
)
|
|
};
|
|
|
|
assert!(result.is_null());
|
|
assert_eq!(
|
|
context.events,
|
|
vec![
|
|
"allocate",
|
|
"workers",
|
|
"initial-state",
|
|
"factory",
|
|
"jobs",
|
|
"buffer-pool",
|
|
"cctx-pool",
|
|
"seq-pool",
|
|
"serial",
|
|
"free",
|
|
]
|
|
);
|
|
}
|
|
|
|
unsafe extern "C" fn create_cctx_test_alloc(_opaque: *mut c_void, _size: usize) -> *mut c_void {
|
|
ptr::null_mut()
|
|
}
|
|
|
|
#[test]
|
|
fn create_cctx_rejects_mismatched_custom_allocator() {
|
|
let mut context = CreateCCtxTestContext {
|
|
factory_ok: true,
|
|
..CreateCCtxTestContext::default()
|
|
};
|
|
let invalid_mem = ZstdCustomMem {
|
|
customAlloc: Some(create_cctx_test_alloc),
|
|
customFree: None,
|
|
opaque: ptr::null_mut(),
|
|
};
|
|
let projection = create_cctx_test_projection(&mut context, invalid_mem);
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_createCCtx(
|
|
&projection,
|
|
Some(create_cctx_test_allocate),
|
|
Some(create_cctx_test_set_workers),
|
|
Some(create_cctx_test_initial_state),
|
|
Some(create_cctx_test_factory),
|
|
Some(create_cctx_test_jobs),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_resource),
|
|
Some(create_cctx_test_serial_init),
|
|
Some(create_cctx_test_free),
|
|
)
|
|
};
|
|
|
|
assert!(result.is_null());
|
|
assert!(context.events.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn free_cctx_preserves_callback_order_and_ownership_conditions() {
|
|
let mut context = FreeCCtxTestContext::default();
|
|
let state = free_cctx_test_state(&mut context, 0, 1);
|
|
|
|
assert_eq!(unsafe { ZSTDMT_rust_freeCCtx(&state) }, 0);
|
|
assert_eq!(
|
|
context.events,
|
|
vec![
|
|
"factory",
|
|
"release-jobs",
|
|
"jobs",
|
|
"buffer-pool",
|
|
"cctx-pool",
|
|
"seq-pool",
|
|
"serial-state",
|
|
"cdict",
|
|
"round-buffer",
|
|
"mtctx",
|
|
]
|
|
);
|
|
|
|
context.events.clear();
|
|
let state = free_cctx_test_state(&mut context, 1, 0);
|
|
assert_eq!(unsafe { ZSTDMT_rust_freeCCtx(&state) }, 0);
|
|
assert_eq!(
|
|
context.events,
|
|
vec![
|
|
"release-jobs",
|
|
"jobs",
|
|
"buffer-pool",
|
|
"cctx-pool",
|
|
"seq-pool",
|
|
"serial-state",
|
|
"cdict",
|
|
"mtctx",
|
|
]
|
|
);
|
|
}
|
|
|
|
struct MockChunkCompressor {
|
|
continue_inputs: Vec<usize>,
|
|
end_inputs: Vec<usize>,
|
|
continue_results: Vec<usize>,
|
|
end_results: Vec<usize>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MockChunkProgress {
|
|
calls: Vec<(usize, usize)>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MockJobProgress {
|
|
events: Vec<&'static str>,
|
|
debug_calls: Vec<(usize, usize)>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MockCompressionJob {
|
|
events: Vec<&'static str>,
|
|
c_size: usize,
|
|
consumed_size: usize,
|
|
expected_src_size: usize,
|
|
}
|
|
|
|
struct WaitForLdmTestContext {
|
|
events: Vec<&'static str>,
|
|
overlaps: Vec<bool>,
|
|
next_overlap: usize,
|
|
}
|
|
|
|
struct SerialWaitForTurnTestContext {
|
|
events: Vec<&'static str>,
|
|
next_job_id: c_uint,
|
|
wake_job_id: c_uint,
|
|
}
|
|
|
|
unsafe extern "C" fn serial_wait_for_turn_test_lock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialWaitForTurnTestContext>())
|
|
.events
|
|
.push("lock")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_wait_for_turn_test_wait(context: *mut c_void) {
|
|
unsafe {
|
|
let context = &mut *context.cast::<SerialWaitForTurnTestContext>();
|
|
context.events.push("wait");
|
|
context.next_job_id = context.wake_job_id;
|
|
}
|
|
}
|
|
|
|
fn serial_wait_for_turn_test_state(
|
|
context: &mut SerialWaitForTurnTestContext,
|
|
job_id: c_uint,
|
|
) -> ZSTDMT_RustSerialWaitForTurnState {
|
|
ZSTDMT_RustSerialWaitForTurnState {
|
|
callback_context: context as *mut _ as *mut c_void,
|
|
next_job_id: &mut context.next_job_id,
|
|
lock: Some(serial_wait_for_turn_test_lock),
|
|
wait: Some(serial_wait_for_turn_test_wait),
|
|
job_id,
|
|
}
|
|
}
|
|
|
|
struct SerialAdvanceTestContext {
|
|
events: Vec<&'static str>,
|
|
}
|
|
|
|
unsafe extern "C" fn serial_advance_test_broadcast(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialAdvanceTestContext>())
|
|
.events
|
|
.push("broadcast")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_advance_test_unlock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialAdvanceTestContext>())
|
|
.events
|
|
.push("unlock")
|
|
};
|
|
}
|
|
|
|
fn serial_advance_test_state(
|
|
context: &mut SerialAdvanceTestContext,
|
|
next_job_id: &mut c_uint,
|
|
) -> ZSTDMT_RustSerialAdvanceState {
|
|
ZSTDMT_RustSerialAdvanceState {
|
|
callback_context: context as *mut _ as *mut c_void,
|
|
next_job_id,
|
|
broadcast: Some(serial_advance_test_broadcast),
|
|
unlock: Some(serial_advance_test_unlock),
|
|
}
|
|
}
|
|
|
|
struct SerialEnsureFinishedTestContext {
|
|
events: Vec<&'static str>,
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_lock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("lock")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_broadcast(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("broadcast")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_ldm_lock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("ldm_lock")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_clear_ldm(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("clear_ldm")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_ldm_signal(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("ldm_signal")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_ldm_unlock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("ldm_unlock")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_unlock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("unlock")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn serial_ensure_finished_test_on_skip(
|
|
context: *mut c_void,
|
|
job_id: c_uint,
|
|
c_size: usize,
|
|
) {
|
|
assert_eq!(job_id, 4);
|
|
assert_eq!(c_size, 123);
|
|
unsafe {
|
|
(*context.cast::<SerialEnsureFinishedTestContext>())
|
|
.events
|
|
.push("skip")
|
|
};
|
|
}
|
|
|
|
fn serial_ensure_finished_test_state(
|
|
context: &mut SerialEnsureFinishedTestContext,
|
|
next_job_id: &mut c_uint,
|
|
job_id: c_uint,
|
|
c_size: usize,
|
|
) -> ZSTDMT_RustSerialEnsureFinishedState {
|
|
ZSTDMT_RustSerialEnsureFinishedState {
|
|
callback_context: context as *mut _ as *mut c_void,
|
|
next_job_id,
|
|
lock: Some(serial_ensure_finished_test_lock),
|
|
broadcast: Some(serial_ensure_finished_test_broadcast),
|
|
ldm_lock: Some(serial_ensure_finished_test_ldm_lock),
|
|
clear_ldm_window: Some(serial_ensure_finished_test_clear_ldm),
|
|
ldm_signal: Some(serial_ensure_finished_test_ldm_signal),
|
|
ldm_unlock: Some(serial_ensure_finished_test_ldm_unlock),
|
|
unlock: Some(serial_ensure_finished_test_unlock),
|
|
on_skip: Some(serial_ensure_finished_test_on_skip),
|
|
c_size,
|
|
job_id,
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn wait_for_ldm_test_lock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<WaitForLdmTestContext>())
|
|
.events
|
|
.push("lock")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn wait_for_ldm_test_overlaps(
|
|
context: *mut c_void,
|
|
_buffer_start: *mut c_void,
|
|
_buffer_capacity: usize,
|
|
) -> c_int {
|
|
let context = unsafe { &mut *context.cast::<WaitForLdmTestContext>() };
|
|
context.events.push("overlap");
|
|
let overlaps = context
|
|
.overlaps
|
|
.get(context.next_overlap)
|
|
.copied()
|
|
.unwrap_or(false);
|
|
context.next_overlap += 1;
|
|
c_int::from(overlaps)
|
|
}
|
|
|
|
unsafe extern "C" fn wait_for_ldm_test_wait(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<WaitForLdmTestContext>())
|
|
.events
|
|
.push("wait")
|
|
};
|
|
}
|
|
|
|
unsafe extern "C" fn wait_for_ldm_test_unlock(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<WaitForLdmTestContext>())
|
|
.events
|
|
.push("unlock")
|
|
};
|
|
}
|
|
|
|
fn wait_for_ldm_test_state(
|
|
context: &mut WaitForLdmTestContext,
|
|
ldm_enabled: c_int,
|
|
) -> ZSTDMT_RustWaitForLdmState {
|
|
ZSTDMT_RustWaitForLdmState {
|
|
callback_context: context as *mut _ as *mut c_void,
|
|
buffer_start: ptr::null_mut(),
|
|
buffer_capacity: 8,
|
|
ldm_enabled,
|
|
lock: Some(wait_for_ldm_test_lock),
|
|
overlaps: Some(wait_for_ldm_test_overlaps),
|
|
wait: Some(wait_for_ldm_test_wait),
|
|
unlock: Some(wait_for_ldm_test_unlock),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wait_for_ldm_runs_overlap_wait_loop_before_unlocking() {
|
|
let mut context = WaitForLdmTestContext {
|
|
events: Vec::new(),
|
|
overlaps: vec![true, false],
|
|
next_overlap: 0,
|
|
};
|
|
let state = wait_for_ldm_test_state(&mut context, ZSTD_PS_ENABLE);
|
|
|
|
unsafe { ZSTDMT_rust_waitForLdmComplete(&state) };
|
|
|
|
assert_eq!(
|
|
context.events,
|
|
vec!["lock", "overlap", "wait", "overlap", "unlock"]
|
|
);
|
|
assert_eq!(context.next_overlap, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn wait_for_ldm_skips_callbacks_when_ldm_is_disabled() {
|
|
let mut context = WaitForLdmTestContext {
|
|
events: Vec::new(),
|
|
overlaps: vec![true],
|
|
next_overlap: 0,
|
|
};
|
|
let state = wait_for_ldm_test_state(&mut context, 0);
|
|
|
|
unsafe { ZSTDMT_rust_waitForLdmComplete(&state) };
|
|
|
|
assert!(context.events.is_empty());
|
|
assert_eq!(context.next_overlap, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_wait_for_turn_waits_until_job_is_ready() {
|
|
let mut context = SerialWaitForTurnTestContext {
|
|
events: Vec::new(),
|
|
next_job_id: 2,
|
|
wake_job_id: 4,
|
|
};
|
|
let state = serial_wait_for_turn_test_state(&mut context, 4);
|
|
|
|
let result = unsafe { ZSTDMT_rust_serialStateWaitForTurn(&state) };
|
|
|
|
assert_eq!(result, 1);
|
|
assert_eq!(context.events, vec!["lock", "wait"]);
|
|
assert_eq!(context.next_job_id, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_wait_for_turn_reports_skip_without_waiting_for_later_job() {
|
|
let mut context = SerialWaitForTurnTestContext {
|
|
events: Vec::new(),
|
|
next_job_id: 5,
|
|
wake_job_id: 5,
|
|
};
|
|
let state = serial_wait_for_turn_test_state(&mut context, 4);
|
|
|
|
let result = unsafe { ZSTDMT_rust_serialStateWaitForTurn(&state) };
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(context.events, vec!["lock"]);
|
|
assert_eq!(context.next_job_id, 5);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_advance_increments_before_broadcast_and_unlock() {
|
|
let mut context = SerialAdvanceTestContext { events: Vec::new() };
|
|
let mut next_job_id = 7;
|
|
let state = serial_advance_test_state(&mut context, &mut next_job_id);
|
|
|
|
unsafe { ZSTDMT_rust_serialStateAdvance(&state) };
|
|
|
|
assert_eq!(next_job_id, 8);
|
|
assert_eq!(context.events, vec!["broadcast", "unlock"]);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_advance_wraps_the_job_counter_like_c_unsigned_arithmetic() {
|
|
let mut context = SerialAdvanceTestContext { events: Vec::new() };
|
|
let mut next_job_id = c_uint::MAX;
|
|
let state = serial_advance_test_state(&mut context, &mut next_job_id);
|
|
|
|
unsafe { ZSTDMT_rust_serialStateAdvance(&state) };
|
|
|
|
assert_eq!(next_job_id, 0);
|
|
assert_eq!(context.events, vec!["broadcast", "unlock"]);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_ensure_finished_runs_failed_job_cleanup_in_order() {
|
|
let mut context = SerialEnsureFinishedTestContext { events: Vec::new() };
|
|
let mut next_job_id = 4;
|
|
let state = serial_ensure_finished_test_state(&mut context, &mut next_job_id, 4, 123);
|
|
|
|
unsafe { ZSTDMT_rust_serialStateEnsureFinishedOrchestrated(&state) };
|
|
|
|
assert_eq!(next_job_id, 5);
|
|
assert_eq!(
|
|
context.events,
|
|
vec![
|
|
"lock",
|
|
"skip",
|
|
"broadcast",
|
|
"ldm_lock",
|
|
"clear_ldm",
|
|
"ldm_signal",
|
|
"ldm_unlock",
|
|
"unlock"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_ensure_finished_unlocks_without_cleanup_for_later_job() {
|
|
let mut context = SerialEnsureFinishedTestContext { events: Vec::new() };
|
|
let mut next_job_id = 6;
|
|
let state = serial_ensure_finished_test_state(&mut context, &mut next_job_id, 4, 123);
|
|
|
|
unsafe { ZSTDMT_rust_serialStateEnsureFinishedOrchestrated(&state) };
|
|
|
|
assert_eq!(next_job_id, 6);
|
|
assert_eq!(context.events, vec!["lock", "unlock"]);
|
|
}
|
|
|
|
fn record_compression_job_event(state: &Rc<RefCell<MockCompressionJob>>, event: &'static str) {
|
|
state.borrow_mut().events.push(event);
|
|
}
|
|
|
|
fn record_compression_job_finish_event(context: *mut c_void, event: &'static str) {
|
|
unsafe {
|
|
(*context.cast::<RefCell<MockCompressionJob>>())
|
|
.borrow_mut()
|
|
.events
|
|
.push(event);
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn compression_job_finish_test_ensure(context: *mut c_void) {
|
|
record_compression_job_finish_event(context, "ensure");
|
|
}
|
|
|
|
unsafe extern "C" fn compression_job_finish_test_release_seq(context: *mut c_void) {
|
|
record_compression_job_finish_event(context, "release-seq");
|
|
}
|
|
|
|
unsafe extern "C" fn compression_job_finish_test_release_cctx(context: *mut c_void) {
|
|
record_compression_job_finish_event(context, "release-cctx");
|
|
}
|
|
|
|
unsafe extern "C" fn compression_job_finish_test_lock(context: *mut c_void) {
|
|
record_compression_job_finish_event(context, "lock");
|
|
}
|
|
|
|
unsafe extern "C" fn compression_job_finish_test_signal(context: *mut c_void) {
|
|
record_compression_job_finish_event(context, "signal");
|
|
}
|
|
|
|
unsafe extern "C" fn compression_job_finish_test_unlock(context: *mut c_void) {
|
|
record_compression_job_finish_event(context, "unlock");
|
|
}
|
|
|
|
unsafe extern "C" fn compression_job_finish_test_validate(
|
|
context: *mut c_void,
|
|
src_size: usize,
|
|
) {
|
|
let state = unsafe { &*context.cast::<RefCell<MockCompressionJob>>() };
|
|
assert_eq!(state.borrow().expected_src_size, src_size);
|
|
record_compression_job_finish_event(context, "validate");
|
|
}
|
|
|
|
fn compression_job_finish_test_projection(
|
|
state: &Rc<RefCell<MockCompressionJob>>,
|
|
src_size: usize,
|
|
) -> ZSTDMT_compressionJobFinishProjection {
|
|
let state_cell = Rc::as_ptr(state);
|
|
let state_value = unsafe { (*state_cell).as_ptr() };
|
|
let (c_size, consumed) = unsafe {
|
|
(*state_value).expected_src_size = src_size;
|
|
(
|
|
&mut (*state_value).c_size as *mut usize,
|
|
&mut (*state_value).consumed_size as *mut usize,
|
|
)
|
|
};
|
|
let callback_context = state_cell.cast_mut().cast();
|
|
ZSTDMT_compressionJobFinishProjection {
|
|
callbackContext: callback_context,
|
|
srcSize: src_size,
|
|
ensureFinished: Some(compression_job_finish_test_ensure),
|
|
publication: ZSTDMT_compressionJobFinishPublicationProjection {
|
|
callbackContext: callback_context,
|
|
cSize: c_size,
|
|
consumed,
|
|
srcSize: src_size,
|
|
lock: Some(compression_job_finish_test_lock),
|
|
signal: Some(compression_job_finish_test_signal),
|
|
unlock: Some(compression_job_finish_test_unlock),
|
|
validateConsumed: Some(compression_job_finish_test_validate),
|
|
},
|
|
releaseSeq: Some(compression_job_finish_test_release_seq),
|
|
releaseCCtx: Some(compression_job_finish_test_release_cctx),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_frame_header_projection_writes_header_and_clears_repcodes() {
|
|
let mut stage = ZSTD_COMPRESSION_STAGE_INIT;
|
|
let mut rep_codes = [11u32, 22, 33];
|
|
let mut output = [0xa5u8; 18];
|
|
let state = ZSTDMT_compressionJobFrameHeaderState {
|
|
dst: output.as_mut_ptr().cast(),
|
|
dst_capacity: output.len(),
|
|
stage: &mut stage,
|
|
no_dict_id_flag: 0,
|
|
checksum_flag: 0,
|
|
content_size_flag: 1,
|
|
format: 0,
|
|
window_log: 20,
|
|
pledged_src_size_plus_one: 1,
|
|
dict_id: 0,
|
|
rep_codes: rep_codes.as_mut_ptr(),
|
|
};
|
|
|
|
let mut created_stage = ZSTD_COMPRESSION_STAGE_CREATED;
|
|
let created_state = ZSTDMT_compressionJobFrameHeaderState {
|
|
stage: &mut created_stage,
|
|
..state
|
|
};
|
|
let created = unsafe { ZSTDMT_rust_writeFrameHeader(&created_state) };
|
|
assert_eq!(created, ERROR(ZstdErrorCode::StageWrong));
|
|
assert_eq!(created_stage, ZSTD_COMPRESSION_STAGE_CREATED);
|
|
assert_eq!(rep_codes, [11, 22, 33]);
|
|
let result = unsafe { ZSTDMT_rust_writeFrameHeader(&state) };
|
|
|
|
assert_eq!(result, 6);
|
|
assert_eq!(stage, ZSTD_COMPRESSION_STAGE_ONGOING);
|
|
assert_eq!(rep_codes, [0; 3]);
|
|
assert_eq!(&output[..result], &[0x28, 0xb5, 0x2f, 0xfd, 0x20, 0x00]);
|
|
assert_eq!(&output[result..], &[0xa5; 12]);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_sequence_gate_skips_empty_store_without_ldm_assertion() {
|
|
let mut calls = Vec::new();
|
|
let projection = ZSTDMT_compressionJobSequenceProjection {
|
|
cctx: 0x10usize as *mut c_void,
|
|
sequences: ptr::null_mut(),
|
|
nbSequences: 0,
|
|
ldmEnabled: 0,
|
|
};
|
|
|
|
apply_compression_job_sequences_with(Some(projection), |cctx, sequences, count| {
|
|
calls.push((cctx, sequences, count));
|
|
});
|
|
|
|
assert!(calls.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_sequence_gate_forwards_nonempty_store_after_ldm_check() {
|
|
let mut calls = Vec::new();
|
|
let projection = ZSTDMT_compressionJobSequenceProjection {
|
|
cctx: 0x10usize as *mut c_void,
|
|
sequences: 0x20usize as *mut c_void,
|
|
nbSequences: 3,
|
|
ldmEnabled: ZSTD_PS_ENABLE,
|
|
};
|
|
|
|
apply_compression_job_sequences_with(Some(projection), |cctx, sequences, count| {
|
|
calls.push((cctx, sequences, count));
|
|
});
|
|
|
|
assert_eq!(
|
|
calls,
|
|
vec![(
|
|
0x10usize as *mut c_void,
|
|
0x20usize as *mut c_void,
|
|
3
|
|
)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_resources_order_pool_gets_destination_and_ldm_check() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let acquire_cctx_events = Rc::clone(&events);
|
|
let acquire_seq_events = Rc::clone(&events);
|
|
let cctx_ready_events = Rc::clone(&events);
|
|
let destination_events = Rc::clone(&events);
|
|
let seq_ready_events = Rc::clone(&events);
|
|
let publish_events = Rc::clone(&events);
|
|
|
|
let result = compression_job_acquire_resources_with(
|
|
ZSTDMT_compressionJobResourceProjection {
|
|
ldmEnabled: 1,
|
|
hasDestination: 0,
|
|
},
|
|
move || acquire_cctx_events.borrow_mut().push("cctx"),
|
|
move || acquire_seq_events.borrow_mut().push("seq"),
|
|
move || {
|
|
cctx_ready_events.borrow_mut().push("cctx-ready");
|
|
true
|
|
},
|
|
move || {
|
|
destination_events.borrow_mut().push("destination");
|
|
0
|
|
},
|
|
move || {
|
|
seq_ready_events.borrow_mut().push("seq-ready");
|
|
true
|
|
},
|
|
move || publish_events.borrow_mut().push("publish"),
|
|
);
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(
|
|
&*events.borrow(),
|
|
&[
|
|
"cctx",
|
|
"seq",
|
|
"cctx-ready",
|
|
"destination",
|
|
"publish",
|
|
"seq-ready",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_resources_reject_missing_cctx_after_both_pool_gets() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let acquire_cctx_events = Rc::clone(&events);
|
|
let acquire_seq_events = Rc::clone(&events);
|
|
let cctx_ready_events = Rc::clone(&events);
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
|
|
let result = compression_job_acquire_resources_with(
|
|
ZSTDMT_compressionJobResourceProjection {
|
|
ldmEnabled: 1,
|
|
hasDestination: 0,
|
|
},
|
|
move || acquire_cctx_events.borrow_mut().push("cctx"),
|
|
move || acquire_seq_events.borrow_mut().push("seq"),
|
|
move || {
|
|
cctx_ready_events.borrow_mut().push("cctx-ready");
|
|
false
|
|
},
|
|
|| panic!("CCtx failure must stop before destination acquisition"),
|
|
|| panic!("CCtx failure must stop before LDM validation"),
|
|
|| panic!("CCtx failure must stop before destination publication"),
|
|
);
|
|
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(&*events.borrow(), &["cctx", "seq", "cctx-ready"]);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_resources_stops_before_publication_on_destination_error() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let acquire_cctx_events = Rc::clone(&events);
|
|
let acquire_seq_events = Rc::clone(&events);
|
|
let cctx_ready_events = Rc::clone(&events);
|
|
let destination_events = Rc::clone(&events);
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
|
|
let result = compression_job_acquire_resources_with(
|
|
ZSTDMT_compressionJobResourceProjection {
|
|
ldmEnabled: 1,
|
|
hasDestination: 0,
|
|
},
|
|
move || acquire_cctx_events.borrow_mut().push("cctx"),
|
|
move || acquire_seq_events.borrow_mut().push("seq"),
|
|
move || {
|
|
cctx_ready_events.borrow_mut().push("cctx-ready");
|
|
true
|
|
},
|
|
move || {
|
|
destination_events.borrow_mut().push("destination");
|
|
expected_error
|
|
},
|
|
|| panic!("destination failure must stop before LDM validation"),
|
|
|| panic!("destination failure must stop before publication"),
|
|
);
|
|
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(
|
|
&*events.borrow(),
|
|
&["cctx", "seq", "cctx-ready", "destination"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_resources_publishes_before_missing_ldm_store_error() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let acquire_cctx_events = Rc::clone(&events);
|
|
let acquire_seq_events = Rc::clone(&events);
|
|
let cctx_ready_events = Rc::clone(&events);
|
|
let seq_ready_events = Rc::clone(&events);
|
|
let publish_events = Rc::clone(&events);
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
|
|
let result = compression_job_acquire_resources_with(
|
|
ZSTDMT_compressionJobResourceProjection {
|
|
ldmEnabled: 1,
|
|
hasDestination: 1,
|
|
},
|
|
move || acquire_cctx_events.borrow_mut().push("cctx"),
|
|
move || acquire_seq_events.borrow_mut().push("seq"),
|
|
move || {
|
|
cctx_ready_events.borrow_mut().push("cctx-ready");
|
|
true
|
|
},
|
|
|| panic!("an existing destination must skip buffer acquisition"),
|
|
move || {
|
|
seq_ready_events.borrow_mut().push("seq-ready");
|
|
false
|
|
},
|
|
move || publish_events.borrow_mut().push("publish"),
|
|
);
|
|
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(
|
|
&*events.borrow(),
|
|
&["cctx", "seq", "cctx-ready", "publish", "seq-ready"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_trace_guards_only_continuation_jobs() {
|
|
let first_job_events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let first_job_assert_events = Rc::clone(&first_job_events);
|
|
let first_job_trace_events = Rc::clone(&first_job_events);
|
|
compression_job_trace_with(
|
|
1,
|
|
move || first_job_assert_events.borrow_mut().push("assert"),
|
|
move || first_job_trace_events.borrow_mut().push("trace"),
|
|
);
|
|
assert_eq!(&*first_job_events.borrow(), &["trace"]);
|
|
|
|
let continuation_events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let continuation_assert_events = Rc::clone(&continuation_events);
|
|
let continuation_trace_events = Rc::clone(&continuation_events);
|
|
compression_job_trace_with(
|
|
0,
|
|
move || continuation_assert_events.borrow_mut().push("assert"),
|
|
move || continuation_trace_events.borrow_mut().push("trace"),
|
|
);
|
|
assert_eq!(&*continuation_events.borrow(), &["assert", "trace"]);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_runs_first_job_stages_and_reports_final_block() {
|
|
let state = Rc::new(RefCell::new(MockCompressionJob::default()));
|
|
let acquire_state = Rc::clone(&state);
|
|
let prepare_state = Rc::clone(&state);
|
|
let sequence_state = Rc::clone(&state);
|
|
let begin_state = Rc::clone(&state);
|
|
let apply_state = Rc::clone(&state);
|
|
let compress_state = Rc::clone(&state);
|
|
let trace_state = Rc::clone(&state);
|
|
let finish = compression_job_finish_test_projection(&state, 19);
|
|
|
|
compression_job_with(
|
|
ZSTDMT_compressionJobProjection {
|
|
firstJob: 1,
|
|
lastJob: 1,
|
|
frameHeaderState: ptr::null(),
|
|
sequenceState: ptr::null(),
|
|
},
|
|
finish,
|
|
move || {
|
|
record_compression_job_event(&acquire_state, "acquire");
|
|
0
|
|
},
|
|
move || record_compression_job_event(&prepare_state, "prepare"),
|
|
move || record_compression_job_event(&sequence_state, "sequences"),
|
|
move || {
|
|
record_compression_job_event(&begin_state, "begin");
|
|
0
|
|
},
|
|
move || record_compression_job_event(&apply_state, "apply"),
|
|
|| panic!("first jobs do not rewrite a frame header"),
|
|
move |last_job| {
|
|
assert_eq!(last_job, 1);
|
|
record_compression_job_event(&compress_state, "compress");
|
|
ZSTDMT_chunkProcessResult {
|
|
error: 0,
|
|
lastBlockSize: 7,
|
|
}
|
|
},
|
|
move || record_compression_job_event(&trace_state, "trace"),
|
|
);
|
|
|
|
let state = state.borrow();
|
|
assert_eq!(
|
|
state.events,
|
|
vec![
|
|
"acquire",
|
|
"prepare",
|
|
"sequences",
|
|
"begin",
|
|
"apply",
|
|
"compress",
|
|
"trace",
|
|
"ensure",
|
|
"release-seq",
|
|
"release-cctx",
|
|
"lock",
|
|
"unlock",
|
|
"validate",
|
|
"lock",
|
|
"unlock",
|
|
"lock",
|
|
"signal",
|
|
"unlock",
|
|
]
|
|
);
|
|
assert_eq!(state.c_size, 7);
|
|
assert_eq!(state.consumed_size, 19);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_normalizes_resource_error_before_finish_cleanup() {
|
|
let state = Rc::new(RefCell::new(MockCompressionJob::default()));
|
|
let acquire_state = Rc::clone(&state);
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
let finish = compression_job_finish_test_projection(&state, 11);
|
|
|
|
compression_job_with(
|
|
ZSTDMT_compressionJobProjection {
|
|
firstJob: 1,
|
|
lastJob: 0,
|
|
frameHeaderState: ptr::null(),
|
|
sequenceState: ptr::null(),
|
|
},
|
|
finish,
|
|
move || {
|
|
record_compression_job_event(&acquire_state, "acquire");
|
|
expected_error
|
|
},
|
|
|| panic!("resource failure must skip parameter preparation"),
|
|
|| panic!("resource failure must skip sequence generation"),
|
|
|| panic!("resource failure must skip context initialization"),
|
|
|| panic!("resource failure must skip sequence application"),
|
|
|| panic!("resource failure must skip frame-header writing"),
|
|
|_| panic!("resource failure must skip compression"),
|
|
|| panic!("resource failure must skip tracing"),
|
|
);
|
|
|
|
let state = state.borrow();
|
|
assert_eq!(
|
|
state.events,
|
|
vec![
|
|
"acquire",
|
|
"lock",
|
|
"unlock",
|
|
"ensure",
|
|
"release-seq",
|
|
"release-cctx",
|
|
"validate",
|
|
"lock",
|
|
"unlock",
|
|
"lock",
|
|
"signal",
|
|
"unlock",
|
|
]
|
|
);
|
|
assert_eq!(state.c_size, expected_error);
|
|
assert_eq!(state.consumed_size, 11);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_turn_runs_ldm_before_checksum_and_advances_once() {
|
|
let events = Rc::new(RefCell::new(Vec::new()));
|
|
let wait_events = Rc::clone(&events);
|
|
let ldm_window_events = Rc::clone(&events);
|
|
let ldm_sequences_events = Rc::clone(&events);
|
|
let ldm_publish_events = Rc::clone(&events);
|
|
let checksum_events = Rc::clone(&events);
|
|
let advance_events = Rc::clone(&events);
|
|
let mut seq_store = ZstdMtRawSeqStore::default();
|
|
|
|
serial_state_gen_sequences_with(
|
|
&mut seq_store,
|
|
ptr::null(),
|
|
8,
|
|
4,
|
|
true,
|
|
true,
|
|
move |job_id| {
|
|
assert_eq!(job_id, 4);
|
|
wait_events.borrow_mut().push("wait");
|
|
true
|
|
},
|
|
move |_seq_store, _src, src_size| {
|
|
assert_eq!(src_size, 8);
|
|
ldm_window_events.borrow_mut().push("ldm-window");
|
|
},
|
|
move |_seq_store, _src, src_size| {
|
|
assert_eq!(src_size, 8);
|
|
ldm_sequences_events.borrow_mut().push("ldm-sequences");
|
|
},
|
|
move || {
|
|
ldm_publish_events.borrow_mut().push("ldm-publish");
|
|
},
|
|
move |_src, src_size| {
|
|
assert_eq!(src_size, 8);
|
|
checksum_events.borrow_mut().push("checksum");
|
|
},
|
|
move || advance_events.borrow_mut().push("advance"),
|
|
);
|
|
|
|
assert_eq!(
|
|
&*events.borrow(),
|
|
&[
|
|
"wait",
|
|
"ldm-window",
|
|
"ldm-sequences",
|
|
"ldm-publish",
|
|
"checksum",
|
|
"advance",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_turn_skips_failed_predecessor_and_keeps_empty_ldm_turn() {
|
|
let events = Rc::new(RefCell::new(Vec::new()));
|
|
let wait_events = Rc::clone(&events);
|
|
let advance_events = Rc::clone(&events);
|
|
let mut seq_store = ZstdMtRawSeqStore::default();
|
|
|
|
serial_state_gen_sequences_with(
|
|
&mut seq_store,
|
|
ptr::null(),
|
|
0,
|
|
9,
|
|
true,
|
|
true,
|
|
move |_job_id| {
|
|
wait_events.borrow_mut().push("wait");
|
|
false
|
|
},
|
|
|_seq_store, _src, _src_size| panic!("skipped jobs must not update the LDM window"),
|
|
|_seq_store, _src, _src_size| {
|
|
panic!("skipped jobs must not generate LDM sequences")
|
|
},
|
|
|| panic!("skipped jobs must not publish the LDM window"),
|
|
|_src, _src_size| panic!("skipped jobs must not update the checksum"),
|
|
move || advance_events.borrow_mut().push("advance"),
|
|
);
|
|
assert_eq!(&*events.borrow(), &["wait", "advance"]);
|
|
|
|
let empty_events = Rc::new(RefCell::new(Vec::new()));
|
|
let empty_ldm_window_events = Rc::clone(&empty_events);
|
|
let empty_ldm_sequences_events = Rc::clone(&empty_events);
|
|
let empty_ldm_publish_events = Rc::clone(&empty_events);
|
|
let empty_advance_events = Rc::clone(&empty_events);
|
|
serial_state_gen_sequences_with(
|
|
&mut seq_store,
|
|
ptr::null(),
|
|
0,
|
|
10,
|
|
true,
|
|
true,
|
|
|_job_id| true,
|
|
move |_seq_store, _src, src_size| {
|
|
assert_eq!(src_size, 0);
|
|
empty_ldm_window_events.borrow_mut().push("ldm-window");
|
|
},
|
|
move |_seq_store, _src, src_size| {
|
|
assert_eq!(src_size, 0);
|
|
empty_ldm_sequences_events
|
|
.borrow_mut()
|
|
.push("ldm-sequences");
|
|
},
|
|
move || {
|
|
empty_ldm_publish_events.borrow_mut().push("ldm-publish");
|
|
},
|
|
|_src, _src_size| panic!("empty input must not update the checksum"),
|
|
move || empty_advance_events.borrow_mut().push("advance"),
|
|
);
|
|
assert_eq!(
|
|
&*empty_events.borrow(),
|
|
&["ldm-window", "ldm-sequences", "ldm-publish", "advance"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_state_ensure_finished_owns_skip_and_wrapping_policy() {
|
|
assert_eq!(
|
|
ZSTDMT_rust_serialStateEnsureFinished(4, 4),
|
|
ZSTDMT_serialStateEnsureFinishedResult {
|
|
skip: 1,
|
|
nextJobID: 5,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_serialStateEnsureFinished(3, 9),
|
|
ZSTDMT_serialStateEnsureFinishedResult {
|
|
skip: 1,
|
|
nextJobID: 10,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_serialStateEnsureFinished(11, 9),
|
|
ZSTDMT_serialStateEnsureFinishedResult {
|
|
skip: 0,
|
|
nextJobID: 11,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_serialStateEnsureFinished(c_uint::MAX, c_uint::MAX),
|
|
ZSTDMT_serialStateEnsureFinishedResult {
|
|
skip: 1,
|
|
nextJobID: 0,
|
|
}
|
|
);
|
|
}
|
|
|
|
struct WaitForAllJobsTestContext {
|
|
events: Vec<(c_uint, c_uint)>,
|
|
}
|
|
|
|
unsafe extern "C" fn wait_for_all_jobs_test_callback(
|
|
context: *mut c_void,
|
|
job_id: c_uint,
|
|
done_job_id: c_uint,
|
|
) {
|
|
unsafe {
|
|
(*context.cast::<WaitForAllJobsTestContext>())
|
|
.events
|
|
.push((job_id, done_job_id));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wait_for_all_jobs_uses_ring_order_and_advances_to_next_job() {
|
|
let mut context = WaitForAllJobsTestContext { events: Vec::new() };
|
|
|
|
let done_job_id = unsafe {
|
|
ZSTDMT_rust_waitForAllJobsCompleted(
|
|
3,
|
|
6,
|
|
3,
|
|
(&mut context as *mut WaitForAllJobsTestContext).cast(),
|
|
Some(wait_for_all_jobs_test_callback),
|
|
)
|
|
};
|
|
|
|
assert_eq!(done_job_id, 6);
|
|
assert_eq!(context.events, vec![(3, 3), (0, 4), (1, 5)]);
|
|
}
|
|
|
|
struct ReleaseAllJobsTestContext {
|
|
job_ids: Vec<c_uint>,
|
|
}
|
|
|
|
unsafe extern "C" fn release_all_jobs_test_callback(context: *mut c_void, job_id: c_uint) {
|
|
unsafe {
|
|
(*context.cast::<ReleaseAllJobsTestContext>())
|
|
.job_ids
|
|
.push(job_id);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn release_all_jobs_visits_each_slot_in_order() {
|
|
let mut context = ReleaseAllJobsTestContext {
|
|
job_ids: Vec::new(),
|
|
};
|
|
|
|
unsafe {
|
|
ZSTDMT_rust_releaseAllJobResources(
|
|
3,
|
|
(&mut context as *mut ReleaseAllJobsTestContext).cast(),
|
|
Some(release_all_jobs_test_callback),
|
|
)
|
|
};
|
|
|
|
assert_eq!(context.job_ids, vec![0, 1, 2, 3]);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_stops_on_non_first_chunk_error_and_cleans_up() {
|
|
let state = Rc::new(RefCell::new(MockCompressionJob::default()));
|
|
let acquire_state = Rc::clone(&state);
|
|
let prepare_state = Rc::clone(&state);
|
|
let sequence_state = Rc::clone(&state);
|
|
let begin_state = Rc::clone(&state);
|
|
let apply_state = Rc::clone(&state);
|
|
let header_state = Rc::clone(&state);
|
|
let compress_state = Rc::clone(&state);
|
|
let expected_error = ERROR(ZstdErrorCode::DstSizeTooSmall);
|
|
let finish = compression_job_finish_test_projection(&state, 23);
|
|
|
|
compression_job_with(
|
|
ZSTDMT_compressionJobProjection {
|
|
firstJob: 0,
|
|
lastJob: 1,
|
|
frameHeaderState: ptr::null(),
|
|
sequenceState: ptr::null(),
|
|
},
|
|
finish,
|
|
move || {
|
|
record_compression_job_event(&acquire_state, "acquire");
|
|
0
|
|
},
|
|
move || record_compression_job_event(&prepare_state, "prepare"),
|
|
move || record_compression_job_event(&sequence_state, "sequences"),
|
|
move || {
|
|
record_compression_job_event(&begin_state, "begin");
|
|
0
|
|
},
|
|
move || record_compression_job_event(&apply_state, "apply"),
|
|
move || {
|
|
record_compression_job_event(&header_state, "header");
|
|
0
|
|
},
|
|
move |last_job| {
|
|
assert_eq!(last_job, 1);
|
|
record_compression_job_event(&compress_state, "compress");
|
|
ZSTDMT_chunkProcessResult {
|
|
error: expected_error,
|
|
lastBlockSize: 9,
|
|
}
|
|
},
|
|
|| panic!("a failed chunk must not be traced"),
|
|
);
|
|
|
|
let state = state.borrow();
|
|
assert_eq!(
|
|
state.events,
|
|
vec![
|
|
"acquire",
|
|
"prepare",
|
|
"sequences",
|
|
"begin",
|
|
"apply",
|
|
"header",
|
|
"compress",
|
|
"lock",
|
|
"unlock",
|
|
"ensure",
|
|
"release-seq",
|
|
"release-cctx",
|
|
"validate",
|
|
"lock",
|
|
"unlock",
|
|
"lock",
|
|
"signal",
|
|
"unlock",
|
|
]
|
|
);
|
|
assert_eq!(state.c_size, expected_error);
|
|
assert_eq!(state.consumed_size, 23);
|
|
}
|
|
|
|
fn init_projection() -> ZSTDMT_initCStreamProjection {
|
|
ZSTDMT_initCStreamProjection {
|
|
requestedNbWorkers: 4,
|
|
currentNbWorkers: 2,
|
|
jobSize: 1,
|
|
jobSizeMin: DEFAULT_ZSTDMT_JOBSIZE_MIN,
|
|
jobSizeMax: DEFAULT_ZSTDMT_JOBSIZE_MAX,
|
|
enableLdm: 0,
|
|
windowLog: 20,
|
|
chainLog: 16,
|
|
strategy: ZSTD_FAST,
|
|
overlapLog: 1,
|
|
rsyncable: 1,
|
|
roundBuffCapacity: 0,
|
|
allJobsCompleted: 0,
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ResetStreamTestContext {
|
|
events: Vec<&'static str>,
|
|
}
|
|
|
|
unsafe extern "C" fn reset_stream_test_round_buffer(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<ResetStreamTestContext>())
|
|
.events
|
|
.push("round");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn reset_stream_test_input_buffer(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<ResetStreamTestContext>())
|
|
.events
|
|
.push("input");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn reset_stream_test_job_ids(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<ResetStreamTestContext>())
|
|
.events
|
|
.push("jobs");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn reset_stream_test_frame_flags(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<ResetStreamTestContext>())
|
|
.events
|
|
.push("flags");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn reset_stream_test_progress(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<ResetStreamTestContext>())
|
|
.events
|
|
.push("progress");
|
|
}
|
|
}
|
|
|
|
fn reset_stream_test_state(context: *mut c_void) -> ZSTDMT_RustResetStreamState {
|
|
ZSTDMT_RustResetStreamState {
|
|
callback_context: context,
|
|
reset_round_buffer: Some(reset_stream_test_round_buffer),
|
|
reset_input_buffer: Some(reset_stream_test_input_buffer),
|
|
reset_job_ids: Some(reset_stream_test_job_ids),
|
|
reset_frame_flags: Some(reset_stream_test_frame_flags),
|
|
reset_progress: Some(reset_stream_test_progress),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reset_stream_preserves_callback_order() {
|
|
let mut context = ResetStreamTestContext::default();
|
|
let state = reset_stream_test_state((&mut context as *mut ResetStreamTestContext).cast());
|
|
|
|
unsafe { ZSTDMT_rust_resetStream(&state) };
|
|
|
|
assert_eq!(
|
|
context.events,
|
|
vec!["round", "input", "jobs", "flags", "progress"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reset_stream_ignores_null_or_incomplete_state() {
|
|
let mut context = ResetStreamTestContext::default();
|
|
let state = ZSTDMT_RustResetStreamState {
|
|
callback_context: (&mut context as *mut ResetStreamTestContext).cast(),
|
|
reset_round_buffer: None,
|
|
reset_input_buffer: Some(reset_stream_test_input_buffer),
|
|
reset_job_ids: Some(reset_stream_test_job_ids),
|
|
reset_frame_flags: Some(reset_stream_test_frame_flags),
|
|
reset_progress: Some(reset_stream_test_progress),
|
|
};
|
|
|
|
unsafe {
|
|
ZSTDMT_rust_resetStream(&state);
|
|
ZSTDMT_rust_resetStream(ptr::null());
|
|
}
|
|
|
|
assert!(context.events.is_empty());
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct SerialResetTestContext {
|
|
events: Vec<&'static str>,
|
|
ldm_params: ZSTDMT_serialResetLdmParameters,
|
|
max_nb_seq: usize,
|
|
resize_result: c_int,
|
|
hash_size: usize,
|
|
num_buckets: usize,
|
|
bucket_log: c_uint,
|
|
previous_bucket_log: c_uint,
|
|
published_job_size: usize,
|
|
published_job_size_u32: u32,
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_set_ldm_params(
|
|
context: *mut c_void,
|
|
enable_ldm: c_int,
|
|
hash_log: c_uint,
|
|
bucket_size_log: c_uint,
|
|
min_match_length: c_uint,
|
|
hash_rate_log: c_uint,
|
|
window_log: c_uint,
|
|
) {
|
|
unsafe {
|
|
let context = &mut *context.cast::<SerialResetTestContext>();
|
|
context.events.push("ldm-params");
|
|
context.ldm_params = ZSTDMT_serialResetLdmParameters {
|
|
enableLdm: enable_ldm,
|
|
hashLog: hash_log,
|
|
bucketSizeLog: bucket_size_log,
|
|
minMatchLength: min_match_length,
|
|
hashRateLog: hash_rate_log,
|
|
windowLog: window_log,
|
|
};
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_next_job(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialResetTestContext>())
|
|
.events
|
|
.push("next-job");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_checksum(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialResetTestContext>())
|
|
.events
|
|
.push("checksum");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_set_nb_seq(context: *mut c_void, max_nb_seq: usize) {
|
|
unsafe {
|
|
let context = &mut *context.cast::<SerialResetTestContext>();
|
|
context.events.push("seq-size");
|
|
context.max_nb_seq = max_nb_seq;
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_window(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialResetTestContext>())
|
|
.events
|
|
.push("window");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_resize(
|
|
context: *mut c_void,
|
|
hash_size: usize,
|
|
num_buckets: usize,
|
|
bucket_log: c_uint,
|
|
previous_bucket_log: c_uint,
|
|
) -> c_int {
|
|
unsafe {
|
|
let context = &mut *context.cast::<SerialResetTestContext>();
|
|
context.events.push("resize");
|
|
context.hash_size = hash_size;
|
|
context.num_buckets = num_buckets;
|
|
context.bucket_log = bucket_log;
|
|
context.previous_bucket_log = previous_bucket_log;
|
|
context.resize_result
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_zero(
|
|
context: *mut c_void,
|
|
_hash_size: usize,
|
|
_num_buckets: usize,
|
|
) {
|
|
unsafe {
|
|
(*context.cast::<SerialResetTestContext>())
|
|
.events
|
|
.push("zero");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_dictionary(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialResetTestContext>())
|
|
.events
|
|
.push("dictionary");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_copy_window(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialResetTestContext>())
|
|
.events
|
|
.push("copy-window");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_reset_test_publish(context: *mut c_void, job_size: usize) {
|
|
unsafe {
|
|
let context = &mut *context.cast::<SerialResetTestContext>();
|
|
context.events.push("publish");
|
|
context.published_job_size = job_size;
|
|
context.published_job_size_u32 = job_size as u32;
|
|
}
|
|
}
|
|
|
|
type SerialResetTestCallbacks = (
|
|
Option<ZSTDMT_serialResetSetLdmParamsFn>,
|
|
Option<ZSTDMT_serialResetVoidFn>,
|
|
Option<ZSTDMT_serialResetVoidFn>,
|
|
Option<ZSTDMT_serialResetSetNbSeqFn>,
|
|
Option<ZSTDMT_serialResetVoidFn>,
|
|
Option<ZSTDMT_serialResetResizeFn>,
|
|
Option<ZSTDMT_serialResetZeroTablesFn>,
|
|
Option<ZSTDMT_serialResetVoidFn>,
|
|
Option<ZSTDMT_serialResetVoidFn>,
|
|
Option<ZSTDMT_serialResetPublishFn>,
|
|
);
|
|
|
|
fn serial_reset_test_callbacks() -> SerialResetTestCallbacks {
|
|
(
|
|
Some(serial_reset_test_set_ldm_params),
|
|
Some(serial_reset_test_next_job),
|
|
Some(serial_reset_test_checksum),
|
|
Some(serial_reset_test_set_nb_seq),
|
|
Some(serial_reset_test_window),
|
|
Some(serial_reset_test_resize),
|
|
Some(serial_reset_test_zero),
|
|
Some(serial_reset_test_dictionary),
|
|
Some(serial_reset_test_copy_window),
|
|
Some(serial_reset_test_publish),
|
|
)
|
|
}
|
|
|
|
fn serial_reset_test_projection(
|
|
enable_ldm: c_int,
|
|
checksum_enabled: c_int,
|
|
job_size: usize,
|
|
) -> ZSTDMT_RustSerialResetProjection {
|
|
ZSTDMT_RustSerialResetProjection {
|
|
enableLdm: enable_ldm,
|
|
checksumEnabled: checksum_enabled,
|
|
jobSize: job_size,
|
|
windowLog: 20,
|
|
strategy: ZSTD_GREEDY,
|
|
hashLog: 0,
|
|
bucketSizeLog: 0,
|
|
minMatchLength: 0,
|
|
hashRateLog: 0,
|
|
previousHashLog: 10,
|
|
previousBucketSizeLog: 4,
|
|
ldmEntrySize: 8,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn serial_state_reset_enabled_prepares_scalars_and_preserves_order() {
|
|
let mut context = SerialResetTestContext::default();
|
|
let callbacks = serial_reset_test_callbacks();
|
|
let projection = serial_reset_test_projection(ZSTD_PS_ENABLE, 1, 640);
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_serialStateReset(
|
|
&projection,
|
|
(&mut context as *mut SerialResetTestContext).cast(),
|
|
callbacks.0,
|
|
callbacks.1,
|
|
callbacks.2,
|
|
callbacks.3,
|
|
callbacks.4,
|
|
callbacks.5,
|
|
callbacks.6,
|
|
callbacks.7,
|
|
callbacks.8,
|
|
callbacks.9,
|
|
)
|
|
};
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(
|
|
context.ldm_params,
|
|
ZSTDMT_serialResetLdmParameters {
|
|
enableLdm: ZSTD_PS_ENABLE,
|
|
hashLog: 14,
|
|
bucketSizeLog: 4,
|
|
minMatchLength: 64,
|
|
hashRateLog: 6,
|
|
windowLog: 20,
|
|
}
|
|
);
|
|
assert_eq!(context.max_nb_seq, 10);
|
|
assert_eq!(context.hash_size, 1 << 14 << 3);
|
|
assert_eq!(context.num_buckets, 1 << 10);
|
|
assert_eq!(context.bucket_log, 10);
|
|
assert_eq!(context.previous_bucket_log, 6);
|
|
assert_eq!(context.published_job_size, 640);
|
|
assert_eq!(
|
|
context.events,
|
|
vec![
|
|
"ldm-params",
|
|
"next-job",
|
|
"checksum",
|
|
"seq-size",
|
|
"window",
|
|
"resize",
|
|
"zero",
|
|
"dictionary",
|
|
"copy-window",
|
|
"publish",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_state_reset_disabled_zeroes_ldm_scalars_and_skips_ldm_work() {
|
|
let mut context = SerialResetTestContext::default();
|
|
let callbacks = serial_reset_test_callbacks();
|
|
let mut projection = serial_reset_test_projection(ZSTD_PS_DISABLE, 0, 640);
|
|
projection.hashLog = 20;
|
|
projection.bucketSizeLog = 4;
|
|
projection.minMatchLength = 32;
|
|
projection.hashRateLog = 5;
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_serialStateReset(
|
|
&projection,
|
|
(&mut context as *mut SerialResetTestContext).cast(),
|
|
callbacks.0,
|
|
callbacks.1,
|
|
callbacks.2,
|
|
callbacks.3,
|
|
callbacks.4,
|
|
callbacks.5,
|
|
callbacks.6,
|
|
callbacks.7,
|
|
callbacks.8,
|
|
callbacks.9,
|
|
)
|
|
};
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(
|
|
context.ldm_params,
|
|
ZSTDMT_serialResetLdmParameters::default()
|
|
);
|
|
assert_eq!(context.max_nb_seq, 0);
|
|
assert_eq!(context.published_job_size, 640);
|
|
assert_eq!(context.events, vec!["ldm-params", "next-job", "publish"]);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_state_reset_preserves_size_t_job_size_overflow_and_cast() {
|
|
let mut context = SerialResetTestContext::default();
|
|
let callbacks = serial_reset_test_callbacks();
|
|
let projection = serial_reset_test_projection(ZSTD_PS_ENABLE, 0, usize::MAX);
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_serialStateReset(
|
|
&projection,
|
|
(&mut context as *mut SerialResetTestContext).cast(),
|
|
callbacks.0,
|
|
callbacks.1,
|
|
callbacks.2,
|
|
callbacks.3,
|
|
callbacks.4,
|
|
callbacks.5,
|
|
callbacks.6,
|
|
callbacks.7,
|
|
callbacks.8,
|
|
callbacks.9,
|
|
)
|
|
};
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(context.max_nb_seq, usize::MAX / 64);
|
|
assert_eq!(context.published_job_size, usize::MAX);
|
|
assert_eq!(context.published_job_size_u32, usize::MAX as u32);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_state_reset_stops_before_zeroing_after_resize_failure() {
|
|
let mut context = SerialResetTestContext {
|
|
resize_result: 1,
|
|
..Default::default()
|
|
};
|
|
let callbacks = serial_reset_test_callbacks();
|
|
let projection = serial_reset_test_projection(ZSTD_PS_ENABLE, 0, 640);
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_serialStateReset(
|
|
&projection,
|
|
(&mut context as *mut SerialResetTestContext).cast(),
|
|
callbacks.0,
|
|
callbacks.1,
|
|
callbacks.2,
|
|
callbacks.3,
|
|
callbacks.4,
|
|
callbacks.5,
|
|
callbacks.6,
|
|
callbacks.7,
|
|
callbacks.8,
|
|
callbacks.9,
|
|
)
|
|
};
|
|
|
|
assert_eq!(result, 1);
|
|
assert_eq!(
|
|
context.events,
|
|
vec!["ldm-params", "next-job", "seq-size", "window", "resize"]
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_initSerialResetResult(result as c_int),
|
|
ERROR(ZstdErrorCode::MemoryAllocation)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serial_dictionary_policy_resets_then_loads_only_raw_content() {
|
|
let events = RefCell::new(Vec::<(&'static str, usize)>::new());
|
|
serial_state_load_dictionary_with(
|
|
ZSTDMT_RustSerialDictionaryProjection {
|
|
dict: ptr::null(),
|
|
dictSize: 12,
|
|
dictContentType: ZSTD_DCT_RAW_CONTENT,
|
|
forceWindow: 0,
|
|
},
|
|
|dict, dict_size| {
|
|
assert!(dict.is_null());
|
|
events.borrow_mut().push(("load", dict_size));
|
|
23
|
|
},
|
|
|loaded_dict_end| events.borrow_mut().push(("end", loaded_dict_end as usize)),
|
|
);
|
|
assert_eq!(
|
|
events.into_inner(),
|
|
vec![("end", 0), ("load", 12), ("end", 23)]
|
|
);
|
|
|
|
let events = RefCell::new(Vec::<(&'static str, usize)>::new());
|
|
serial_state_load_dictionary_with(
|
|
ZSTDMT_RustSerialDictionaryProjection {
|
|
dict: ptr::null(),
|
|
dictSize: 12,
|
|
dictContentType: ZSTD_DCT_RAW_CONTENT,
|
|
forceWindow: 1,
|
|
},
|
|
|_, dict_size| {
|
|
events.borrow_mut().push(("load", dict_size));
|
|
23
|
|
},
|
|
|loaded_dict_end| events.borrow_mut().push(("end", loaded_dict_end as usize)),
|
|
);
|
|
assert_eq!(events.into_inner(), vec![("end", 0), ("load", 12)]);
|
|
|
|
let events = RefCell::new(Vec::<(&'static str, usize)>::new());
|
|
serial_state_load_dictionary_with(
|
|
ZSTDMT_RustSerialDictionaryProjection {
|
|
dict: ptr::null(),
|
|
dictSize: 12,
|
|
dictContentType: 2,
|
|
forceWindow: 0,
|
|
},
|
|
|_, _| panic!("non-raw dictionary must not be loaded"),
|
|
|loaded_dict_end| events.borrow_mut().push(("end", loaded_dict_end as usize)),
|
|
);
|
|
assert_eq!(events.into_inner(), vec![("end", 0)]);
|
|
|
|
let events = RefCell::new(Vec::<(&'static str, usize)>::new());
|
|
serial_state_load_dictionary_with(
|
|
ZSTDMT_RustSerialDictionaryProjection {
|
|
dict: ptr::null(),
|
|
dictSize: 0,
|
|
dictContentType: ZSTD_DCT_RAW_CONTENT,
|
|
forceWindow: 0,
|
|
},
|
|
|_, _| panic!("empty dictionary must not be loaded"),
|
|
|loaded_dict_end| events.borrow_mut().push(("end", loaded_dict_end as usize)),
|
|
);
|
|
assert_eq!(events.into_inner(), vec![("end", 0)]);
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct SerialLifecycleTestContext {
|
|
events: Vec<&'static str>,
|
|
init_errors: [c_int; 4],
|
|
}
|
|
|
|
unsafe fn serial_lifecycle_init_step(
|
|
context: *mut c_void,
|
|
index: usize,
|
|
event: &'static str,
|
|
) -> c_int {
|
|
let context = unsafe { &mut *context.cast::<SerialLifecycleTestContext>() };
|
|
context.events.push(event);
|
|
context.init_errors[index]
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_zero(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialLifecycleTestContext>())
|
|
.events
|
|
.push("zero");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_init_mutex(context: *mut c_void) -> c_int {
|
|
unsafe { serial_lifecycle_init_step(context, 0, "mutex") }
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_init_cond(context: *mut c_void) -> c_int {
|
|
unsafe { serial_lifecycle_init_step(context, 1, "cond") }
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_init_ldm_mutex(context: *mut c_void) -> c_int {
|
|
unsafe { serial_lifecycle_init_step(context, 2, "ldm-mutex") }
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_init_ldm_cond(context: *mut c_void) -> c_int {
|
|
unsafe { serial_lifecycle_init_step(context, 3, "ldm-cond") }
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_destroy_mutex(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialLifecycleTestContext>())
|
|
.events
|
|
.push("destroy-mutex");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_destroy_cond(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialLifecycleTestContext>())
|
|
.events
|
|
.push("destroy-cond");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_destroy_ldm_mutex(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialLifecycleTestContext>())
|
|
.events
|
|
.push("destroy-ldm-mutex");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_destroy_ldm_cond(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialLifecycleTestContext>())
|
|
.events
|
|
.push("destroy-ldm-cond");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn serial_lifecycle_free_tables(context: *mut c_void) {
|
|
unsafe {
|
|
(*context.cast::<SerialLifecycleTestContext>())
|
|
.events
|
|
.push("free-tables");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn serial_state_lifecycle_preserves_order_and_aggregates_init_errors() {
|
|
let mut context = SerialLifecycleTestContext {
|
|
init_errors: [0, 2, 0, 4],
|
|
..Default::default()
|
|
};
|
|
let opaque = (&mut context as *mut SerialLifecycleTestContext).cast();
|
|
|
|
let result = unsafe {
|
|
ZSTDMT_rust_serialStateInit(
|
|
opaque,
|
|
Some(serial_lifecycle_zero),
|
|
Some(serial_lifecycle_init_mutex),
|
|
Some(serial_lifecycle_init_cond),
|
|
Some(serial_lifecycle_init_ldm_mutex),
|
|
Some(serial_lifecycle_init_ldm_cond),
|
|
)
|
|
};
|
|
assert_eq!(result, 6);
|
|
|
|
unsafe {
|
|
ZSTDMT_rust_serialStateFree(
|
|
opaque,
|
|
Some(serial_lifecycle_destroy_mutex),
|
|
Some(serial_lifecycle_destroy_cond),
|
|
Some(serial_lifecycle_destroy_ldm_mutex),
|
|
Some(serial_lifecycle_destroy_ldm_cond),
|
|
Some(serial_lifecycle_free_tables),
|
|
)
|
|
};
|
|
|
|
assert_eq!(
|
|
context.events,
|
|
vec![
|
|
"zero",
|
|
"mutex",
|
|
"cond",
|
|
"ldm-mutex",
|
|
"ldm-cond",
|
|
"destroy-mutex",
|
|
"destroy-cond",
|
|
"destroy-ldm-mutex",
|
|
"destroy-ldm-cond",
|
|
"free-tables",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn init_serial_reset_result_maps_nonzero_status_to_memory_error() {
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
|
|
assert_eq!(init_serial_reset_result(0), 0);
|
|
assert_eq!(ZSTDMT_rust_initSerialResetResult(0), 0);
|
|
for status in [1, -1, 2] {
|
|
assert_eq!(init_serial_reset_result(status), expected_error);
|
|
assert_eq!(ZSTDMT_rust_initSerialResetResult(status), expected_error);
|
|
}
|
|
}
|
|
|
|
fn mt_cparams_update_projection() -> ZSTDMT_CParamsUpdateProjection {
|
|
ZSTDMT_CParamsUpdateProjection {
|
|
compressionLevel: 3,
|
|
cctxSrcSizeHint: 0,
|
|
srcSizeHint: u64::MAX,
|
|
dictSize: 0,
|
|
mode: 0,
|
|
enableLdm: ZSTD_PS_DISABLE,
|
|
ldmDefaultWindowLog: 27,
|
|
overrides: ZSTD_compressionParameters::default(),
|
|
useRowMatchFinder: 0,
|
|
exclusionMask: 0,
|
|
savedWindowLog: 18,
|
|
}
|
|
}
|
|
|
|
fn expected_mt_cparams_update(
|
|
projection: ZSTDMT_CParamsUpdateProjection,
|
|
) -> ZSTD_compressionParameters {
|
|
ZSTD_rust_params_updateCParamsWhileCompressing(
|
|
ZSTD_rust_params_updateCParamsWhileCompressingState {
|
|
compressionLevel: projection.compressionLevel,
|
|
cctxSrcSizeHint: projection.cctxSrcSizeHint,
|
|
srcSizeHint: projection.srcSizeHint,
|
|
dictSize: projection.dictSize,
|
|
mode: projection.mode,
|
|
enableLdm: projection.enableLdm,
|
|
ldmDefaultWindowLog: projection.ldmDefaultWindowLog,
|
|
overrides: projection.overrides,
|
|
useRowMatchFinder: projection.useRowMatchFinder,
|
|
exclusionMask: projection.exclusionMask,
|
|
savedWindowLog: projection.savedWindowLog,
|
|
},
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn mt_update_parameters_carries_level_with_next_job_params() {
|
|
let projection = mt_cparams_update_projection();
|
|
let expected = expected_mt_cparams_update(projection);
|
|
let result = update_cparams_while_compressing(projection);
|
|
|
|
assert_eq!(result.compressionLevel, projection.compressionLevel);
|
|
assert_eq!(result.cParams, expected);
|
|
assert_eq!(result.cParams.windowLog, projection.savedWindowLog);
|
|
assert_eq!(
|
|
ZSTDMT_rust_updateCParamsWhileCompressing(projection),
|
|
result
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mt_update_parameters_uses_context_size_hint_only_for_unknown_size() {
|
|
let mut from_context_hint = mt_cparams_update_projection();
|
|
from_context_hint.cctxSrcSizeHint = 1 << 20;
|
|
|
|
let mut explicit_size = mt_cparams_update_projection();
|
|
explicit_size.srcSizeHint = 1 << 20;
|
|
|
|
assert_eq!(
|
|
update_cparams_while_compressing(from_context_hint).cParams,
|
|
update_cparams_while_compressing(explicit_size).cParams
|
|
);
|
|
|
|
let mut explicit_with_stale_hint = explicit_size;
|
|
explicit_with_stale_hint.cctxSrcSizeHint = 1;
|
|
assert_eq!(
|
|
update_cparams_while_compressing(explicit_size).cParams,
|
|
update_cparams_while_compressing(explicit_with_stale_hint).cParams
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mt_update_parameters_restores_active_window_after_ldm_and_overrides() {
|
|
let mut projection = mt_cparams_update_projection();
|
|
projection.compressionLevel = -1;
|
|
projection.enableLdm = ZSTD_PS_ENABLE;
|
|
projection.ldmDefaultWindowLog = 27;
|
|
projection.overrides.strategy = ZSTD_BTULTRA2;
|
|
projection.savedWindowLog = 19;
|
|
|
|
let result = update_cparams_while_compressing(projection);
|
|
|
|
assert_eq!(result.compressionLevel, -1);
|
|
assert_eq!(result.cParams.strategy, ZSTD_BTULTRA2);
|
|
assert_eq!(result.cParams.windowLog, projection.savedWindowLog);
|
|
assert_ne!(result.cParams.windowLog, projection.ldmDefaultWindowLog);
|
|
}
|
|
|
|
#[test]
|
|
fn dictionary_policy_classifies_copy_borrow_and_raw_prefix_branches() {
|
|
assert_eq!(
|
|
classify_prepare_dictionary(ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 1,
|
|
rawContent: 0,
|
|
}),
|
|
PrepareDictionaryAction::CreateCopied
|
|
);
|
|
assert_eq!(
|
|
classify_prepare_dictionary(ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 0,
|
|
rawContent: 1,
|
|
}),
|
|
PrepareDictionaryAction::AttachBorrowed
|
|
);
|
|
|
|
for (projection, expected) in [
|
|
(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 0,
|
|
rawContent: 0,
|
|
},
|
|
UpdateDictionaryAction::AttachBorrowed,
|
|
),
|
|
(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 1,
|
|
rawContent: 1,
|
|
},
|
|
UpdateDictionaryAction::SetRawPrefix,
|
|
),
|
|
(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 1,
|
|
rawContent: 0,
|
|
},
|
|
UpdateDictionaryAction::CreateReferenced,
|
|
),
|
|
] {
|
|
assert_eq!(classify_update_dictionary(projection), expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_dictionary_releases_before_copy_or_borrow_action() {
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
let events = RefCell::new(Vec::new());
|
|
let result = prepare_c_stream_dictionary_with(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 1,
|
|
rawContent: 0,
|
|
},
|
|
|| events.borrow_mut().push("release"),
|
|
|| {
|
|
events.borrow_mut().push("copy");
|
|
expected_error
|
|
},
|
|
|| panic!("copy branch must not attach borrowed dictionary"),
|
|
);
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(events.into_inner(), vec!["release", "copy"]);
|
|
|
|
let events = RefCell::new(Vec::new());
|
|
let result = prepare_c_stream_dictionary_with(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 0,
|
|
rawContent: 0,
|
|
},
|
|
|| events.borrow_mut().push("release"),
|
|
|| panic!("borrow branch must not create a copied dictionary"),
|
|
|| events.borrow_mut().push("borrow"),
|
|
);
|
|
assert_eq!(result, 0);
|
|
assert_eq!(events.into_inner(), vec!["release", "borrow"]);
|
|
}
|
|
|
|
#[test]
|
|
fn update_dictionary_clears_before_borrow_raw_or_reference_action() {
|
|
let events = RefCell::new(Vec::new());
|
|
let result = update_c_stream_dictionary_with(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 0,
|
|
rawContent: 0,
|
|
},
|
|
|| events.borrow_mut().push("clear"),
|
|
|| events.borrow_mut().push("borrow"),
|
|
|| panic!("borrow branch must not set a raw prefix"),
|
|
|| panic!("borrow branch must not create a referenced dictionary"),
|
|
);
|
|
assert_eq!(result, 0);
|
|
assert_eq!(events.into_inner(), vec!["clear", "borrow"]);
|
|
|
|
let events = RefCell::new(Vec::new());
|
|
let result = update_c_stream_dictionary_with(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 1,
|
|
rawContent: 1,
|
|
},
|
|
|| events.borrow_mut().push("clear"),
|
|
|| panic!("raw-prefix branch must not attach a borrowed CDict"),
|
|
|| events.borrow_mut().push("raw"),
|
|
|| panic!("raw-prefix branch must not create a referenced dictionary"),
|
|
);
|
|
assert_eq!(result, 0);
|
|
assert_eq!(events.into_inner(), vec!["clear", "raw"]);
|
|
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
let events = RefCell::new(Vec::new());
|
|
let result = update_c_stream_dictionary_with(
|
|
ZSTDMT_RustDictionaryProjection {
|
|
hasDictionary: 1,
|
|
rawContent: 0,
|
|
},
|
|
|| events.borrow_mut().push("clear"),
|
|
|| panic!("reference branch must not attach a borrowed CDict"),
|
|
|| panic!("reference branch must not set a raw prefix"),
|
|
|| {
|
|
events.borrow_mut().push("reference");
|
|
expected_error
|
|
},
|
|
);
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(events.into_inner(), vec!["clear", "reference"]);
|
|
}
|
|
|
|
#[test]
|
|
fn init_c_stream_preserves_success_order_and_normalization() {
|
|
let projection = init_projection();
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let values = Rc::new(RefCell::new(Vec::<(&'static str, usize)>::new()));
|
|
let resize_events = Rc::clone(&events);
|
|
let wait_events = Rc::clone(&events);
|
|
let release_events = Rc::clone(&events);
|
|
let apply_events = Rc::clone(&events);
|
|
let apply_values = Rc::clone(&values);
|
|
let prepare_events = Rc::clone(&events);
|
|
let prefix_events = Rc::clone(&events);
|
|
let prefix_values = Rc::clone(&values);
|
|
let section_events = Rc::clone(&events);
|
|
let section_values = Rc::clone(&values);
|
|
let rsync_events = Rc::clone(&events);
|
|
let buffer_events = Rc::clone(&events);
|
|
let buffer_values = Rc::clone(&values);
|
|
let round_events = Rc::clone(&events);
|
|
let round_values = Rc::clone(&values);
|
|
let reset_events = Rc::clone(&events);
|
|
let update_events = Rc::clone(&events);
|
|
let serial_events = Rc::clone(&events);
|
|
let serial_values = Rc::clone(&values);
|
|
|
|
let result = init_c_stream_with(
|
|
projection,
|
|
move |workers| {
|
|
assert_eq!(workers, 4);
|
|
resize_events.borrow_mut().push("resize");
|
|
0
|
|
},
|
|
move || wait_events.borrow_mut().push("wait"),
|
|
move || release_events.borrow_mut().push("release"),
|
|
move |job_size| {
|
|
apply_events.borrow_mut().push("apply");
|
|
apply_values.borrow_mut().push(("job", job_size));
|
|
},
|
|
move || {
|
|
prepare_events.borrow_mut().push("prepare-dict");
|
|
0
|
|
},
|
|
move |size| {
|
|
prefix_events.borrow_mut().push("prefix");
|
|
prefix_values.borrow_mut().push(("prefix", size));
|
|
},
|
|
move |size| {
|
|
section_events.borrow_mut().push("section");
|
|
section_values.borrow_mut().push(("section", size));
|
|
},
|
|
move |_hit_mask, _prime_power| rsync_events.borrow_mut().push("rsync"),
|
|
move |size| {
|
|
buffer_events.borrow_mut().push("buffer");
|
|
buffer_values.borrow_mut().push(("buffer", size));
|
|
},
|
|
move |capacity| {
|
|
round_events.borrow_mut().push("round");
|
|
round_values.borrow_mut().push(("round", capacity));
|
|
0
|
|
},
|
|
move || reset_events.borrow_mut().push("reset"),
|
|
move || {
|
|
update_events.borrow_mut().push("update-dict");
|
|
0
|
|
},
|
|
move |size| {
|
|
serial_events.borrow_mut().push("serial");
|
|
serial_values.borrow_mut().push(("serial", size));
|
|
0
|
|
},
|
|
);
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(
|
|
events.borrow().as_slice(),
|
|
&[
|
|
"resize",
|
|
"wait",
|
|
"release",
|
|
"apply",
|
|
"prepare-dict",
|
|
"prefix",
|
|
"section",
|
|
"rsync",
|
|
"buffer",
|
|
"round",
|
|
"reset",
|
|
"update-dict",
|
|
"serial",
|
|
]
|
|
);
|
|
assert_eq!(values.borrow()[0], ("job", DEFAULT_ZSTDMT_JOBSIZE_MIN));
|
|
assert_eq!(values.borrow()[1], ("prefix", 0));
|
|
assert_eq!(values.borrow()[2], ("section", DEFAULT_ZSTDMT_JOBSIZE_MIN));
|
|
assert_eq!(values.borrow()[5], ("serial", DEFAULT_ZSTDMT_JOBSIZE_MIN));
|
|
assert!(values.borrow()[3].1 > 0);
|
|
assert!(values.borrow()[4].1 > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn init_c_stream_stops_before_normalization_when_resize_fails() {
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
let result = init_c_stream_with(
|
|
init_projection(),
|
|
|_| expected_error,
|
|
|| panic!("a failed resize must stop initialization"),
|
|
|| panic!("a failed resize must stop initialization"),
|
|
|_| panic!("a failed resize must stop initialization"),
|
|
|| panic!("a failed resize must stop initialization"),
|
|
|_| panic!("a failed resize must stop initialization"),
|
|
|_| panic!("a failed resize must stop initialization"),
|
|
|_, _| panic!("a failed resize must stop initialization"),
|
|
|_| panic!("a failed resize must stop initialization"),
|
|
|_| panic!("a failed resize must stop initialization"),
|
|
|| panic!("a failed resize must stop initialization"),
|
|
|| panic!("a failed resize must stop initialization"),
|
|
|_| panic!("a failed resize must stop initialization"),
|
|
);
|
|
|
|
assert_eq!(result, expected_error);
|
|
}
|
|
|
|
#[test]
|
|
fn init_c_stream_stops_after_round_buffer_error() {
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
let mut projection = init_projection();
|
|
projection.currentNbWorkers = projection.requestedNbWorkers;
|
|
projection.allJobsCompleted = 1;
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let result = init_c_stream_with(
|
|
projection,
|
|
|_| panic!("workers already match"),
|
|
|| panic!("all jobs are complete"),
|
|
|| panic!("all jobs are complete"),
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move |_| events.borrow_mut().push("apply")
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move || {
|
|
events.borrow_mut().push("prepare");
|
|
0
|
|
}
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move |_| events.borrow_mut().push("prefix")
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move |_| events.borrow_mut().push("section")
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move |_, _| events.borrow_mut().push("rsync")
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move |_| events.borrow_mut().push("buffer")
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move |_| {
|
|
events.borrow_mut().push("round");
|
|
expected_error
|
|
}
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move || events.borrow_mut().push("reset")
|
|
},
|
|
{
|
|
let events = Rc::clone(&events);
|
|
move || {
|
|
events.borrow_mut().push("update");
|
|
0
|
|
}
|
|
},
|
|
|_| panic!("serial reset must follow successful dictionary update"),
|
|
);
|
|
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(
|
|
events.borrow().as_slice(),
|
|
&["apply", "prepare", "prefix", "section", "rsync", "buffer", "round"]
|
|
);
|
|
}
|
|
|
|
unsafe extern "C" fn mock_compress_continue(
|
|
cctx: *mut c_void,
|
|
_dst: *mut c_void,
|
|
_dst_capacity: usize,
|
|
_src: *const c_void,
|
|
src_size: usize,
|
|
) -> usize {
|
|
let compressor = unsafe { &mut *cctx.cast::<MockChunkCompressor>() };
|
|
let call_number = compressor.continue_inputs.len();
|
|
compressor.continue_inputs.push(src_size);
|
|
compressor
|
|
.continue_results
|
|
.get(call_number)
|
|
.copied()
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
unsafe extern "C" fn mock_compress_end(
|
|
cctx: *mut c_void,
|
|
_dst: *mut c_void,
|
|
_dst_capacity: usize,
|
|
_src: *const c_void,
|
|
src_size: usize,
|
|
) -> usize {
|
|
let compressor = unsafe { &mut *cctx.cast::<MockChunkCompressor>() };
|
|
let call_number = compressor.end_inputs.len();
|
|
compressor.end_inputs.push(src_size);
|
|
compressor
|
|
.end_results
|
|
.get(call_number)
|
|
.copied()
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
unsafe extern "C" fn mock_chunk_progress(context: *mut c_void, c_size: usize, consumed: usize) {
|
|
let progress = unsafe { &mut *context.cast::<MockChunkProgress>() };
|
|
progress.calls.push((c_size, consumed));
|
|
}
|
|
|
|
unsafe extern "C" fn mock_job_progress_lock(context: *mut c_void) {
|
|
unsafe { (*context.cast::<MockJobProgress>()).events.push("lock") };
|
|
}
|
|
|
|
unsafe extern "C" fn mock_job_progress_signal(context: *mut c_void) {
|
|
unsafe { (*context.cast::<MockJobProgress>()).events.push("signal") };
|
|
}
|
|
|
|
unsafe extern "C" fn mock_job_progress_unlock(context: *mut c_void) {
|
|
unsafe { (*context.cast::<MockJobProgress>()).events.push("unlock") };
|
|
}
|
|
|
|
unsafe extern "C" fn mock_job_progress_debug(
|
|
context: *mut c_void,
|
|
c_size: usize,
|
|
total_c_size: usize,
|
|
) {
|
|
let progress = unsafe { &mut *context.cast::<MockJobProgress>() };
|
|
progress.events.push("debug");
|
|
progress.debug_calls.push((c_size, total_c_size));
|
|
}
|
|
|
|
fn run_mock_chunk_loop(
|
|
compressor: &mut MockChunkCompressor,
|
|
progress: &mut MockChunkProgress,
|
|
src_size: usize,
|
|
chunk_size: usize,
|
|
last_job: c_uint,
|
|
) -> ZSTDMT_chunkProcessResult {
|
|
let src = [0u8; 32];
|
|
let mut dst = [0u8; 64];
|
|
unsafe {
|
|
compress_job_chunks_with(
|
|
(compressor as *mut MockChunkCompressor).cast(),
|
|
src.as_ptr().cast(),
|
|
src_size,
|
|
dst.as_mut_ptr().cast(),
|
|
dst.len(),
|
|
chunk_size,
|
|
last_job,
|
|
(progress as *mut MockChunkProgress).cast(),
|
|
Some(mock_chunk_progress),
|
|
mock_compress_continue,
|
|
mock_compress_end,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_progress_preserves_publication_order_and_wraps_c_size() {
|
|
let mut c_size = usize::MAX - 1;
|
|
let mut consumed = 0;
|
|
let mut progress = MockJobProgress::default();
|
|
let projection = ZSTDMT_compressionJobProgressProjection {
|
|
callbackContext: (&mut progress as *mut MockJobProgress).cast(),
|
|
cSize: &mut c_size,
|
|
consumed: &mut consumed,
|
|
lock: Some(mock_job_progress_lock),
|
|
signal: Some(mock_job_progress_signal),
|
|
unlock: Some(mock_job_progress_unlock),
|
|
debug: Some(mock_job_progress_debug),
|
|
};
|
|
|
|
unsafe {
|
|
ZSTDMT_rust_compressionJobProgress(
|
|
(&projection as *const ZSTDMT_compressionJobProgressProjection)
|
|
.cast_mut()
|
|
.cast(),
|
|
3,
|
|
17,
|
|
)
|
|
};
|
|
|
|
assert_eq!(c_size, 1);
|
|
assert_eq!(consumed, 17);
|
|
assert_eq!(progress.events, ["lock", "debug", "signal", "unlock"]);
|
|
assert_eq!(progress.debug_calls, [(3, 1)]);
|
|
}
|
|
|
|
fn run_output_publication(
|
|
output: &mut [u8],
|
|
output_size: usize,
|
|
output_pos: usize,
|
|
job: &[u8],
|
|
job_capacity: usize,
|
|
c_size: usize,
|
|
dst_flushed: usize,
|
|
) -> ZSTDMT_flushPublicationResult {
|
|
unsafe {
|
|
publish_job_output_with(
|
|
output.as_mut_ptr().cast(),
|
|
output_size,
|
|
output_pos,
|
|
job.as_ptr().cast(),
|
|
job_capacity,
|
|
c_size,
|
|
dst_flushed,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn output_publication_handles_no_output_space() {
|
|
let mut output = [0xa5; 8];
|
|
let original = output;
|
|
let job = [1, 2, 3, 4];
|
|
let output_size = output.len();
|
|
|
|
let result = run_output_publication(
|
|
&mut output,
|
|
output_size,
|
|
output_size,
|
|
&job,
|
|
job.len(),
|
|
job.len(),
|
|
0,
|
|
);
|
|
|
|
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
|
|
assert_eq!(result.toFlush, 0);
|
|
assert_eq!(result.outputPos, output.len());
|
|
assert_eq!(result.dstFlushed, 0);
|
|
assert_eq!(output, original);
|
|
}
|
|
|
|
#[test]
|
|
fn output_publication_handles_partial_flush_with_offsets() {
|
|
let mut output = [0xa5; 10];
|
|
let job = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17];
|
|
|
|
let result = run_output_publication(&mut output, 9, 2, &job, job.len(), 6, 1);
|
|
|
|
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
|
|
assert_eq!(result.toFlush, 5);
|
|
assert_eq!(result.outputPos, 7);
|
|
assert_eq!(result.dstFlushed, 6);
|
|
assert_eq!(&output[..2], &[0xa5, 0xa5]);
|
|
assert_eq!(&output[2..7], &job[1..6]);
|
|
assert_eq!(&output[7..], &[0xa5, 0xa5, 0xa5]);
|
|
}
|
|
|
|
#[test]
|
|
fn output_publication_handles_complete_flush() {
|
|
let mut output = [0xa5; 8];
|
|
let job = [0x20, 0x21, 0x22, 0x23, 0x24, 0x25];
|
|
let output_size = output.len();
|
|
|
|
let result = run_output_publication(&mut output, output_size, 1, &job, job.len(), 6, 2);
|
|
|
|
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
|
|
assert_eq!(result.toFlush, 4);
|
|
assert_eq!(result.outputPos, 5);
|
|
assert_eq!(result.dstFlushed, 6);
|
|
assert_eq!(&output[1..5], &job[2..6]);
|
|
assert_eq!(&output[..1], &[0xa5]);
|
|
assert_eq!(&output[5..], &[0xa5, 0xa5, 0xa5]);
|
|
}
|
|
|
|
#[test]
|
|
fn output_publication_handles_repeated_partial_flushes() {
|
|
let mut output = [0xa5; 8];
|
|
let job = [0x30, 0x31, 0x32, 0x33, 0x34, 0x35];
|
|
|
|
let first = run_output_publication(&mut output, 3, 0, &job, job.len(), 6, 0);
|
|
let second = run_output_publication(
|
|
&mut output,
|
|
6,
|
|
first.outputPos,
|
|
&job,
|
|
job.len(),
|
|
6,
|
|
first.dstFlushed,
|
|
);
|
|
|
|
assert_eq!(first.status, FLUSH_PUBLICATION_OK);
|
|
assert_eq!(first.toFlush, 3);
|
|
assert_eq!(first.outputPos, 3);
|
|
assert_eq!(first.dstFlushed, 3);
|
|
assert_eq!(second.status, FLUSH_PUBLICATION_OK);
|
|
assert_eq!(second.toFlush, 3);
|
|
assert_eq!(second.outputPos, 6);
|
|
assert_eq!(second.dstFlushed, 6);
|
|
assert_eq!(&output[..6], &job);
|
|
assert_eq!(&output[6..], &[0xa5, 0xa5]);
|
|
}
|
|
|
|
#[test]
|
|
fn output_publication_does_not_copy_trailing_sentinels() {
|
|
let mut output = [0xa5; 12];
|
|
let job = [0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0xde, 0xad];
|
|
let output_size = output.len();
|
|
|
|
let result = run_output_publication(&mut output, output_size, 4, &job, job.len(), 5, 2);
|
|
|
|
assert_eq!(result.status, FLUSH_PUBLICATION_OK);
|
|
assert_eq!(result.toFlush, 3);
|
|
assert_eq!(&output[..4], &[0xa5, 0xa5, 0xa5, 0xa5]);
|
|
assert_eq!(&output[4..7], &job[2..5]);
|
|
assert_eq!(&output[7..], &[0xa5, 0xa5, 0xa5, 0xa5, 0xa5]);
|
|
}
|
|
|
|
#[test]
|
|
fn output_publication_rejects_invalid_bounds_without_copy() {
|
|
let mut output = [0xa5; 8];
|
|
let original = output;
|
|
let job = [0x50, 0x51, 0x52, 0x53];
|
|
|
|
let result = run_output_publication(&mut output, 4, 5, &job, job.len(), 4, 0);
|
|
|
|
assert_eq!(result.status, FLUSH_PUBLICATION_INVALID_BOUNDS);
|
|
assert_eq!(result.toFlush, 0);
|
|
assert_eq!(result.outputPos, 5);
|
|
assert_eq!(result.dstFlushed, 0);
|
|
assert_eq!(output, original);
|
|
}
|
|
|
|
#[test]
|
|
fn create_job_rejects_full_table_without_callbacks() {
|
|
let projection = ZSTDMT_createJobProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 8,
|
|
jobIDMask: 7,
|
|
..ZSTDMT_createJobProjection::default()
|
|
};
|
|
let result = create_compression_job_with(
|
|
projection,
|
|
|_job_id, _initialization| panic!("table-full job must not be prepared"),
|
|
|_job_id| panic!("table-full job must not be written"),
|
|
|_job_id| panic!("table-full job must not be posted"),
|
|
);
|
|
|
|
assert_eq!(result.action, CREATE_JOB_TABLE_FULL);
|
|
assert_eq!(result.jobID, 0);
|
|
assert_eq!(result.jobNumber, 8);
|
|
assert_eq!(result.nextJobID, 8);
|
|
assert_eq!(result.jobReady, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn create_job_projects_ordinary_initialization_and_prefix_advance() {
|
|
let source = [0u8; 8];
|
|
let prefix = [1u8; 3];
|
|
let projection = ZSTDMT_createJobProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 3,
|
|
jobIDMask: 7,
|
|
srcStart: source.as_ptr().cast(),
|
|
srcSize: source.len(),
|
|
inBuffFilled: source.len(),
|
|
prefixStart: prefix.as_ptr().cast(),
|
|
prefixSize: prefix.len(),
|
|
targetPrefixSize: 4,
|
|
checksumFlag: 1,
|
|
..ZSTDMT_createJobProjection::default()
|
|
};
|
|
let mut initialization = None;
|
|
let mut posted = None;
|
|
let result = create_compression_job_with(
|
|
projection,
|
|
|job_id, value| {
|
|
assert_eq!(job_id, 3);
|
|
initialization = Some(*value);
|
|
},
|
|
|_job_id| panic!("ordinary job must not use the empty-block path"),
|
|
|job_id| {
|
|
posted = Some(job_id);
|
|
true
|
|
},
|
|
);
|
|
|
|
let initialization = initialization.expect("job should be initialized");
|
|
assert_eq!(initialization.srcStart, source.as_ptr().cast());
|
|
assert_eq!(initialization.srcSize, source.len());
|
|
assert_eq!(initialization.prefixStart, prefix.as_ptr().cast());
|
|
assert_eq!(initialization.prefixSize, prefix.len());
|
|
assert_eq!(
|
|
initialization.nextPrefixStart,
|
|
source.as_ptr().wrapping_add(4).cast()
|
|
);
|
|
assert_eq!(initialization.nextPrefixSize, 4);
|
|
assert_eq!(initialization.roundBuffPosDelta, source.len());
|
|
assert_eq!(initialization.jobNumber, 3);
|
|
assert_eq!(initialization.firstJob, 0);
|
|
assert_eq!(initialization.lastJob, 0);
|
|
assert_eq!(initialization.frameChecksumNeeded, 0);
|
|
assert_eq!(posted, Some(3));
|
|
assert_eq!(result.action, CREATE_JOB_POST);
|
|
assert_eq!(result.jobID, 3);
|
|
assert_eq!(result.nextJobID, 4);
|
|
assert_eq!(result.jobReady, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn create_job_projects_terminal_empty_frame() {
|
|
let prefix = [2u8; 2];
|
|
let projection = ZSTDMT_createJobProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 2,
|
|
jobIDMask: 7,
|
|
inBuffFilled: 0,
|
|
prefixStart: prefix.as_ptr().cast(),
|
|
prefixSize: prefix.len(),
|
|
endFrame: 1,
|
|
checksumFlag: 1,
|
|
..ZSTDMT_createJobProjection::default()
|
|
};
|
|
let mut initialization = None;
|
|
let mut empty_job = None;
|
|
let result = create_compression_job_with(
|
|
projection,
|
|
|job_id, value| {
|
|
assert_eq!(job_id, 2);
|
|
initialization = Some(*value);
|
|
},
|
|
|job_id| empty_job = Some(job_id),
|
|
|_job_id| panic!("terminal empty job must not be posted"),
|
|
);
|
|
|
|
let initialization = initialization.expect("empty job should be initialized");
|
|
assert_eq!(initialization.srcSize, 0);
|
|
assert_eq!(initialization.prefixStart, prefix.as_ptr().cast());
|
|
assert_eq!(initialization.prefixSize, prefix.len());
|
|
assert!(initialization.nextPrefixStart.is_null());
|
|
assert_eq!(initialization.nextPrefixSize, 0);
|
|
assert_eq!(initialization.roundBuffPosDelta, 0);
|
|
assert_eq!(initialization.jobNumber, 2);
|
|
assert_eq!(initialization.firstJob, 0);
|
|
assert_eq!(initialization.lastJob, 1);
|
|
assert_eq!(initialization.frameChecksumNeeded, 1);
|
|
assert_eq!(initialization.clearChecksumFlag, 0);
|
|
assert_eq!(empty_job, Some(2));
|
|
assert_eq!(result.action, CREATE_JOB_EMPTY);
|
|
assert_eq!(result.nextJobID, 3);
|
|
assert_eq!(result.jobReady, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_frame_state_classifies_all_flag_combinations() {
|
|
for (last_job, clear_checksum_flag, expected) in [
|
|
(0, 0, CompressionJobFrameStateAction::None),
|
|
(0, 1, CompressionJobFrameStateAction::None),
|
|
(1, 0, CompressionJobFrameStateAction::SetFrameEnded),
|
|
(
|
|
1,
|
|
1,
|
|
CompressionJobFrameStateAction::SetFrameEndedAndClearChecksum,
|
|
),
|
|
] {
|
|
assert_eq!(
|
|
classify_compression_job_frame_state(last_job, clear_checksum_flag),
|
|
expected
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn job_preparation_orders_descriptor_input_and_terminal_state() {
|
|
let initialization = ZSTDMT_jobInitialization {
|
|
jobNumber: 7,
|
|
lastJob: 2,
|
|
clearChecksumFlag: 1,
|
|
..ZSTDMT_jobInitialization::default()
|
|
};
|
|
let events = RefCell::new(Vec::new());
|
|
|
|
prepare_compression_job_with(
|
|
3,
|
|
&initialization,
|
|
|job_id, value| {
|
|
assert_eq!(job_id, 3);
|
|
assert_eq!(value.jobNumber, 7);
|
|
events.borrow_mut().push("descriptor");
|
|
},
|
|
|job_id, value| {
|
|
assert_eq!(job_id, 3);
|
|
assert_eq!(value.roundBuffPosDelta, 0);
|
|
events.borrow_mut().push("input");
|
|
},
|
|
|| events.borrow_mut().push("frame-ended"),
|
|
|| events.borrow_mut().push("checksum"),
|
|
);
|
|
|
|
assert_eq!(
|
|
events.into_inner(),
|
|
vec!["descriptor", "input", "frame-ended", "checksum"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn job_preparation_skips_frame_state_for_nonterminal_job() {
|
|
let initialization = ZSTDMT_jobInitialization::default();
|
|
let events = RefCell::new(Vec::new());
|
|
|
|
prepare_compression_job_with(
|
|
4,
|
|
&initialization,
|
|
|_job_id, _value| events.borrow_mut().push("descriptor"),
|
|
|_job_id, _value| events.borrow_mut().push("input"),
|
|
|| panic!("nonterminal job must not set frame-ended state"),
|
|
|| panic!("nonterminal job must not clear checksum state"),
|
|
);
|
|
|
|
assert_eq!(events.into_inner(), vec!["descriptor", "input"]);
|
|
}
|
|
|
|
#[test]
|
|
fn create_job_preserves_pool_success_and_failure_state() {
|
|
let projection = ZSTDMT_createJobProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 4,
|
|
jobIDMask: 7,
|
|
jobReady: 1,
|
|
..ZSTDMT_createJobProjection::default()
|
|
};
|
|
let success = create_compression_job_with(
|
|
projection,
|
|
|_job_id, _initialization| panic!("prepared job must not be reinitialized"),
|
|
|_job_id| panic!("prepared job must not use the empty-block path"),
|
|
|job_id| {
|
|
assert_eq!(job_id, 4);
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(success.action, CREATE_JOB_POST);
|
|
assert_eq!(success.nextJobID, 5);
|
|
assert_eq!(success.jobReady, 0);
|
|
|
|
let failure = create_compression_job_with(
|
|
projection,
|
|
|_job_id, _initialization| panic!("prepared job must not be reinitialized"),
|
|
|_job_id| panic!("prepared job must not use the empty-block path"),
|
|
|job_id| {
|
|
assert_eq!(job_id, 4);
|
|
false
|
|
},
|
|
);
|
|
assert_eq!(failure.action, CREATE_JOB_POST);
|
|
assert_eq!(failure.nextJobID, 4);
|
|
assert_eq!(failure.jobReady, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn flush_state_machine_preserves_offsets_and_completes_job() {
|
|
let job = [0x60u8, 0x61, 0x62, 0x63, 0x64, 0x65];
|
|
let mut output = [0xa5u8; 8];
|
|
let projection = ZSTDMT_flushJobProjection {
|
|
consumed: 4,
|
|
cSize: job.len(),
|
|
srcSize: 4,
|
|
dstStart: job.as_ptr().cast(),
|
|
dstCapacity: job.len(),
|
|
dstFlushed: 2,
|
|
frameChecksumNeeded: 0,
|
|
};
|
|
let context = ZSTDMT_flushContextProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 1,
|
|
jobIDMask: 0,
|
|
..ZSTDMT_flushContextProjection::default()
|
|
};
|
|
let mut updated = None;
|
|
let mut completed = None;
|
|
|
|
let result = unsafe {
|
|
flush_produced_with(
|
|
context,
|
|
output.as_mut_ptr().cast(),
|
|
output.len(),
|
|
1,
|
|
0,
|
|
1,
|
|
|job_id, block_to_flush| {
|
|
assert_eq!(job_id, 0);
|
|
assert_eq!(block_to_flush, 0);
|
|
projection
|
|
},
|
|
|_job_id, _projection| panic!("unexpected checksum callback"),
|
|
|_job_id, dst_flushed| updated = Some(dst_flushed),
|
|
|_job_id, src_size, c_size| completed = Some((src_size, c_size)),
|
|
|| panic!("unexpected wait callback"),
|
|
|| panic!("unexpected release callback"),
|
|
)
|
|
};
|
|
|
|
assert_eq!(result.result, 0);
|
|
assert_eq!(result.outputPos, 5);
|
|
assert_eq!(result.updateAllJobsCompleted, 1);
|
|
assert_eq!(result.allJobsCompleted, 0);
|
|
assert_eq!(updated, Some(6));
|
|
assert_eq!(completed, Some((4, 6)));
|
|
assert_eq!(&output[1..5], &job[2..]);
|
|
assert_eq!(&output[..1], &[0xa5]);
|
|
}
|
|
|
|
#[test]
|
|
fn flush_state_machine_keeps_pending_output_without_space() {
|
|
let job = [0x70u8, 0x71, 0x72, 0x73, 0x74, 0x75];
|
|
let mut output = [0xa5u8; 4];
|
|
let original = output;
|
|
let projection = ZSTDMT_flushJobProjection {
|
|
consumed: 2,
|
|
cSize: job.len(),
|
|
srcSize: 5,
|
|
dstStart: job.as_ptr().cast(),
|
|
dstCapacity: job.len(),
|
|
dstFlushed: 2,
|
|
frameChecksumNeeded: 0,
|
|
};
|
|
let context = ZSTDMT_flushContextProjection {
|
|
doneJobID: 3,
|
|
nextJobID: 4,
|
|
jobIDMask: 0,
|
|
..ZSTDMT_flushContextProjection::default()
|
|
};
|
|
let mut updated = None;
|
|
let mut completed = false;
|
|
|
|
let result = unsafe {
|
|
flush_produced_with(
|
|
context,
|
|
output.as_mut_ptr().cast(),
|
|
output.len(),
|
|
output.len(),
|
|
1,
|
|
1,
|
|
|_job_id, _block_to_flush| projection,
|
|
|_job_id, _projection| panic!("unexpected checksum callback"),
|
|
|_job_id, dst_flushed| updated = Some(dst_flushed),
|
|
|_job_id, _src_size, _c_size| completed = true,
|
|
|| panic!("unexpected wait callback"),
|
|
|| panic!("unexpected release callback"),
|
|
)
|
|
};
|
|
|
|
assert_eq!(result.result, 4);
|
|
assert_eq!(result.outputPos, output.len());
|
|
assert_eq!(result.updateAllJobsCompleted, 0);
|
|
assert_eq!(updated, Some(2));
|
|
assert!(!completed);
|
|
assert_eq!(output, original);
|
|
}
|
|
|
|
#[test]
|
|
fn flush_state_machine_normalizes_worker_error_and_checksum_projection() {
|
|
let error_projection = ZSTDMT_flushJobProjection {
|
|
cSize: ERROR(ZstdErrorCode::Generic),
|
|
..ZSTDMT_flushJobProjection::default()
|
|
};
|
|
let context = ZSTDMT_flushContextProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 1,
|
|
jobIDMask: 0,
|
|
..ZSTDMT_flushContextProjection::default()
|
|
};
|
|
let error_cleanup = Rc::new(RefCell::new(Vec::new()));
|
|
let wait_cleanup = Rc::clone(&error_cleanup);
|
|
let release_cleanup = Rc::clone(&error_cleanup);
|
|
let result = unsafe {
|
|
flush_produced_with(
|
|
context,
|
|
ptr::null_mut(),
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
|_job_id, _block_to_flush| error_projection,
|
|
|_job_id, _projection| panic!("unexpected checksum callback"),
|
|
|_job_id, _dst_flushed| panic!("unexpected update callback"),
|
|
|_job_id, _src_size, _c_size| panic!("unexpected completion callback"),
|
|
move || wait_cleanup.borrow_mut().push("wait"),
|
|
move || release_cleanup.borrow_mut().push("release"),
|
|
)
|
|
};
|
|
assert_eq!(result.result, ERROR(ZstdErrorCode::Generic));
|
|
assert_eq!(result.outputPos, 0);
|
|
assert_eq!(&*error_cleanup.borrow(), &["wait", "release"]);
|
|
|
|
let job = [0x80u8, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86];
|
|
let mut output = [0xa5u8; 8];
|
|
let projection = ZSTDMT_flushJobProjection {
|
|
consumed: 4,
|
|
cSize: 3,
|
|
srcSize: 4,
|
|
dstStart: job.as_ptr().cast(),
|
|
dstCapacity: job.len(),
|
|
dstFlushed: 0,
|
|
frameChecksumNeeded: 1,
|
|
};
|
|
let mut checksum_calls = 0;
|
|
let mut completed_size = None;
|
|
let result = unsafe {
|
|
flush_produced_with(
|
|
ZSTDMT_flushContextProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 1,
|
|
jobIDMask: 0,
|
|
..ZSTDMT_flushContextProjection::default()
|
|
},
|
|
output.as_mut_ptr().cast(),
|
|
output.len(),
|
|
0,
|
|
0,
|
|
1,
|
|
|_job_id, _block_to_flush| projection,
|
|
|_job_id, projected| {
|
|
checksum_calls += 1;
|
|
projected.cSize = 7;
|
|
projected.frameChecksumNeeded = 0;
|
|
},
|
|
|_job_id, _dst_flushed| {},
|
|
|_job_id, _src_size, c_size| completed_size = Some(c_size),
|
|
|| panic!("unexpected wait callback"),
|
|
|| panic!("unexpected release callback"),
|
|
)
|
|
};
|
|
assert_eq!(checksum_calls, 1);
|
|
assert_eq!(completed_size, Some(7));
|
|
assert_eq!(result.result, 0);
|
|
assert_eq!(&output[..7], &job);
|
|
}
|
|
|
|
#[test]
|
|
fn flush_state_machine_propagates_checksum_projection_error() {
|
|
let job = [0x80u8, 0x81, 0x82, 0x83];
|
|
let mut output = [0xa5u8; 8];
|
|
let projection = ZSTDMT_flushJobProjection {
|
|
consumed: job.len(),
|
|
cSize: 3,
|
|
srcSize: job.len(),
|
|
dstStart: job.as_ptr().cast(),
|
|
dstCapacity: job.len(),
|
|
frameChecksumNeeded: 1,
|
|
..ZSTDMT_flushJobProjection::default()
|
|
};
|
|
let error_cleanup = Rc::new(RefCell::new(Vec::new()));
|
|
let wait_cleanup = Rc::clone(&error_cleanup);
|
|
let release_cleanup = Rc::clone(&error_cleanup);
|
|
let mut completed = false;
|
|
|
|
let result = unsafe {
|
|
flush_produced_with(
|
|
ZSTDMT_flushContextProjection {
|
|
doneJobID: 0,
|
|
nextJobID: 1,
|
|
jobIDMask: 0,
|
|
..ZSTDMT_flushContextProjection::default()
|
|
},
|
|
output.as_mut_ptr().cast(),
|
|
output.len(),
|
|
0,
|
|
0,
|
|
1,
|
|
|_job_id, _block_to_flush| projection,
|
|
|_job_id, projected| {
|
|
projected.cSize = ERROR(ZstdErrorCode::DstSizeTooSmall);
|
|
},
|
|
|_job_id, _dst_flushed| panic!("unexpected update callback"),
|
|
|_job_id, _src_size, _c_size| completed = true,
|
|
move || wait_cleanup.borrow_mut().push("wait"),
|
|
move || release_cleanup.borrow_mut().push("release"),
|
|
)
|
|
};
|
|
|
|
assert_eq!(result.result, ERROR(ZstdErrorCode::DstSizeTooSmall));
|
|
assert_eq!(result.outputPos, 0);
|
|
assert_eq!(&*error_cleanup.borrow(), &["wait", "release"]);
|
|
assert!(!completed);
|
|
assert_eq!(output, [0xa5; 8]);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_checksum_is_serialized_little_endian() {
|
|
let mut dst = [0xa5u8; 8];
|
|
let projection = ZSTDMT_frameChecksumProjection {
|
|
dst: dst.as_mut_ptr().cast(),
|
|
dstCapacity: dst.len(),
|
|
cSize: 4,
|
|
checksum: 0x7856_3412,
|
|
};
|
|
|
|
assert_eq!(write_frame_checksum(&projection), 8);
|
|
assert_eq!(&dst[..4], &[0xa5; 4]);
|
|
assert_eq!(&dst[4..], &[0x12, 0x34, 0x56, 0x78]);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_checksum_accepts_exact_destination_capacity() {
|
|
let mut dst = [0xa5u8; 6];
|
|
let projection = ZSTDMT_frameChecksumProjection {
|
|
dst: dst.as_mut_ptr().cast(),
|
|
dstCapacity: dst.len(),
|
|
cSize: 2,
|
|
checksum: 0x1122_3344,
|
|
};
|
|
|
|
assert_eq!(write_frame_checksum(&projection), dst.len());
|
|
assert_eq!(&dst, &[0xa5, 0xa5, 0x44, 0x33, 0x22, 0x11]);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_checksum_rejects_too_small_or_null_destination() {
|
|
let mut dst = [0xa5u8; 8];
|
|
let too_small = ZSTDMT_frameChecksumProjection {
|
|
dst: dst.as_mut_ptr().cast(),
|
|
dstCapacity: 7,
|
|
cSize: 4,
|
|
checksum: 0x0102_0304,
|
|
};
|
|
let null_destination = ZSTDMT_frameChecksumProjection {
|
|
dst: ptr::null_mut(),
|
|
dstCapacity: 4,
|
|
cSize: 0,
|
|
checksum: 0x0102_0304,
|
|
};
|
|
|
|
assert_eq!(
|
|
write_frame_checksum(&too_small),
|
|
ERROR(ZstdErrorCode::DstSizeTooSmall)
|
|
);
|
|
assert_eq!(
|
|
write_frame_checksum(&null_destination),
|
|
ERROR(ZstdErrorCode::DstSizeTooSmall)
|
|
);
|
|
assert_eq!(dst, [0xa5; 8]);
|
|
}
|
|
|
|
#[test]
|
|
fn flush_state_machine_reports_end_of_frame_state() {
|
|
let not_ended = unsafe {
|
|
flush_produced_with(
|
|
ZSTDMT_flushContextProjection {
|
|
frameEnded: 0,
|
|
..ZSTDMT_flushContextProjection::default()
|
|
},
|
|
ptr::null_mut(),
|
|
0,
|
|
0,
|
|
0,
|
|
ZSTD_E_END,
|
|
|_job_id, _block_to_flush| panic!("no job expected"),
|
|
|_job_id, _projection| panic!("no checksum expected"),
|
|
|_job_id, _dst_flushed| panic!("no update expected"),
|
|
|_job_id, _src_size, _c_size| panic!("no completion expected"),
|
|
|| panic!("no wait expected"),
|
|
|| panic!("no release expected"),
|
|
)
|
|
};
|
|
assert_eq!(not_ended.result, 1);
|
|
assert_eq!(not_ended.updateAllJobsCompleted, 1);
|
|
assert_eq!(not_ended.allJobsCompleted, 0);
|
|
|
|
let ended = unsafe {
|
|
flush_produced_with(
|
|
ZSTDMT_flushContextProjection {
|
|
frameEnded: 1,
|
|
..ZSTDMT_flushContextProjection::default()
|
|
},
|
|
ptr::null_mut(),
|
|
0,
|
|
0,
|
|
0,
|
|
ZSTD_E_END,
|
|
|_job_id, _block_to_flush| panic!("no job expected"),
|
|
|_job_id, _projection| panic!("no checksum expected"),
|
|
|_job_id, _dst_flushed| panic!("no update expected"),
|
|
|_job_id, _src_size, _c_size| panic!("no completion expected"),
|
|
|| panic!("no wait expected"),
|
|
|| panic!("no release expected"),
|
|
)
|
|
};
|
|
assert_eq!(ended.result, 0);
|
|
assert_eq!(ended.updateAllJobsCompleted, 1);
|
|
assert_eq!(ended.allJobsCompleted, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn outer_scheduler_copies_input_and_reports_filled_after_progress() {
|
|
let input_bytes = [1u8, 2, 3, 4, 5];
|
|
let mut round_buffer = [0xa5u8; 4];
|
|
let mut output_buffer = [0xb6u8; 8];
|
|
let context = ZSTDMT_compressStreamContextProjection {
|
|
frameEnded: 0,
|
|
jobReady: 0,
|
|
inBuffStart: ptr::null_mut(),
|
|
inBuffCapacity: 0,
|
|
inBuffFilled: 0,
|
|
targetSectionSize: round_buffer.len(),
|
|
rsyncable: 0,
|
|
..ZSTDMT_compressStreamContextProjection::default()
|
|
};
|
|
let input = ZSTDMT_streamInputProjection {
|
|
src: input_bytes.as_ptr().cast(),
|
|
size: input_bytes.len(),
|
|
pos: 0,
|
|
};
|
|
let output = ZSTDMT_streamOutputProjection {
|
|
dst: output_buffer.as_mut_ptr().cast(),
|
|
size: output_buffer.len(),
|
|
pos: 1,
|
|
};
|
|
let mut range_calls = 0;
|
|
let mut create_calls = Vec::new();
|
|
let mut flush_calls = Vec::new();
|
|
let round_buffer_ptr = round_buffer.as_mut_ptr();
|
|
let round_capacity = round_buffer.len();
|
|
|
|
let result = unsafe {
|
|
compress_stream_generic_with(
|
|
context,
|
|
input,
|
|
output,
|
|
ZSTD_E_CONTINUE,
|
|
|projection| {
|
|
range_calls += 1;
|
|
projection.bufferStart = round_buffer_ptr.cast();
|
|
projection.bufferCapacity = round_capacity;
|
|
projection.bufferFilled = 0;
|
|
1
|
|
},
|
|
|src_size, end| {
|
|
create_calls.push((src_size, end));
|
|
ZSTDMT_streamCreateJobResult {
|
|
result: 0,
|
|
inBuffActive: 1,
|
|
}
|
|
},
|
|
|_dst, _size, output_pos, block_to_flush, end| {
|
|
flush_calls.push((block_to_flush, end));
|
|
ZSTDMT_streamFlushResult {
|
|
result: 0,
|
|
outputPos: output_pos + 2,
|
|
}
|
|
},
|
|
|_context, input, projection| ZSTDMT_syncPointProjection {
|
|
toLoad: (input.size - input.pos).min(round_capacity - projection.bufferFilled),
|
|
flush: 0,
|
|
},
|
|
)
|
|
};
|
|
|
|
assert_eq!(range_calls, 1);
|
|
assert_eq!(round_buffer, input_bytes[..round_buffer.len()]);
|
|
assert_eq!(create_calls, vec![(round_buffer.len(), ZSTD_E_CONTINUE)]);
|
|
assert_eq!(flush_calls, vec![(0, ZSTD_E_CONTINUE)]);
|
|
assert_eq!(result.result, 1);
|
|
assert_eq!(result.inputPos, round_buffer.len());
|
|
assert_eq!(result.outputPos, 3);
|
|
assert_eq!(result.inBuffFilled, round_buffer.len());
|
|
assert_eq!(output_buffer[0], 0xb6);
|
|
}
|
|
|
|
#[test]
|
|
fn outer_scheduler_suppresses_filled_after_job_clears_range() {
|
|
let mut round_buffer = [0xa5u8; 4];
|
|
let context = ZSTDMT_compressStreamContextProjection {
|
|
inBuffStart: round_buffer.as_mut_ptr().cast(),
|
|
inBuffCapacity: round_buffer.len(),
|
|
inBuffFilled: 2,
|
|
targetSectionSize: round_buffer.len(),
|
|
..ZSTDMT_compressStreamContextProjection::default()
|
|
};
|
|
let mut create_call = None;
|
|
|
|
let result = unsafe {
|
|
compress_stream_generic_with(
|
|
context,
|
|
ZSTDMT_streamInputProjection::default(),
|
|
ZSTDMT_streamOutputProjection::default(),
|
|
ZSTD_E_FLUSH,
|
|
|_projection| panic!("the active range must not be reacquired"),
|
|
|src_size, end| {
|
|
create_call = Some((src_size, end));
|
|
ZSTDMT_streamCreateJobResult {
|
|
result: 0,
|
|
inBuffActive: 0,
|
|
}
|
|
},
|
|
|_dst, _size, output_pos, _block_to_flush, _end| ZSTDMT_streamFlushResult {
|
|
result: 0,
|
|
outputPos: output_pos,
|
|
},
|
|
|_context, _input, _projection| panic!("no synchronization scan expected"),
|
|
)
|
|
};
|
|
|
|
assert_eq!(create_call, Some((2, ZSTD_E_FLUSH)));
|
|
assert_eq!(result.result, 0);
|
|
assert_eq!(result.inBuffFilled, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn outer_scheduler_forces_flush_before_end_when_input_remains() {
|
|
let input_bytes = [7u8, 8, 9, 10, 11];
|
|
let mut round_buffer = [0xa5u8; 3];
|
|
let context = ZSTDMT_compressStreamContextProjection {
|
|
inBuffStart: round_buffer.as_mut_ptr().cast(),
|
|
inBuffCapacity: round_buffer.len(),
|
|
targetSectionSize: round_buffer.len(),
|
|
rsyncable: 0,
|
|
..ZSTDMT_compressStreamContextProjection::default()
|
|
};
|
|
let input = ZSTDMT_streamInputProjection {
|
|
src: input_bytes.as_ptr().cast(),
|
|
size: input_bytes.len(),
|
|
pos: 0,
|
|
};
|
|
let output = ZSTDMT_streamOutputProjection::default();
|
|
let mut create_end = None;
|
|
let mut flush_end = None;
|
|
let mut flush_block = None;
|
|
|
|
let result = unsafe {
|
|
compress_stream_generic_with(
|
|
context,
|
|
input,
|
|
output,
|
|
ZSTD_E_END,
|
|
|_projection| panic!("input range should already be available"),
|
|
|src_size, end| {
|
|
create_end = Some((src_size, end));
|
|
ZSTDMT_streamCreateJobResult {
|
|
result: 0,
|
|
inBuffActive: 0,
|
|
}
|
|
},
|
|
|_dst, _size, output_pos, block_to_flush, end| {
|
|
flush_block = Some(block_to_flush);
|
|
flush_end = Some(end);
|
|
ZSTDMT_streamFlushResult {
|
|
result: 0,
|
|
outputPos: output_pos,
|
|
}
|
|
},
|
|
|_context, input, projection| ZSTDMT_syncPointProjection {
|
|
toLoad: (input.size - input.pos)
|
|
.min(round_buffer.len() - projection.bufferFilled),
|
|
flush: 0,
|
|
},
|
|
)
|
|
};
|
|
|
|
assert_eq!(create_end, Some((round_buffer.len(), ZSTD_E_FLUSH)));
|
|
assert_eq!(flush_end, Some(ZSTD_E_FLUSH));
|
|
assert_eq!(flush_block, Some(0));
|
|
assert_eq!(result.result, 1);
|
|
assert_eq!(result.inputPos, round_buffer.len());
|
|
}
|
|
|
|
#[test]
|
|
fn outer_scheduler_resubmits_ready_job_and_blocks_for_flush_progress() {
|
|
let context = ZSTDMT_compressStreamContextProjection {
|
|
jobReady: 1,
|
|
targetSectionSize: 4,
|
|
..ZSTDMT_compressStreamContextProjection::default()
|
|
};
|
|
let input_bytes = [0x31u8, 0x32];
|
|
let input = ZSTDMT_streamInputProjection {
|
|
src: input_bytes.as_ptr().cast(),
|
|
size: input_bytes.len(),
|
|
pos: 0,
|
|
};
|
|
let output = ZSTDMT_streamOutputProjection::default();
|
|
let mut create_call = None;
|
|
let mut flush_call = None;
|
|
|
|
let result = unsafe {
|
|
compress_stream_generic_with(
|
|
context,
|
|
input,
|
|
output,
|
|
ZSTD_E_CONTINUE,
|
|
|_projection| panic!("a ready job must skip input acquisition"),
|
|
|src_size, end| {
|
|
create_call = Some((src_size, end));
|
|
ZSTDMT_streamCreateJobResult {
|
|
result: 0,
|
|
inBuffActive: 0,
|
|
}
|
|
},
|
|
|_dst, _size, output_pos, block_to_flush, end| {
|
|
flush_call = Some((block_to_flush, end));
|
|
ZSTDMT_streamFlushResult {
|
|
result: 1,
|
|
outputPos: output_pos,
|
|
}
|
|
},
|
|
|_context, _input, _projection| panic!("ready job must skip sync scan"),
|
|
)
|
|
};
|
|
|
|
assert_eq!(create_call, Some((0, ZSTD_E_CONTINUE)));
|
|
assert_eq!(flush_call, Some((1, ZSTD_E_CONTINUE)));
|
|
assert_eq!(result.result, 1);
|
|
assert_eq!(result.inputPos, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn outer_scheduler_stops_before_flush_on_job_error() {
|
|
let mut round_buffer = [0xa5u8; 2];
|
|
let input_bytes = [0x41u8, 0x42];
|
|
let context = ZSTDMT_compressStreamContextProjection {
|
|
inBuffStart: round_buffer.as_mut_ptr().cast(),
|
|
inBuffCapacity: round_buffer.len(),
|
|
targetSectionSize: round_buffer.len(),
|
|
..ZSTDMT_compressStreamContextProjection::default()
|
|
};
|
|
let input = ZSTDMT_streamInputProjection {
|
|
src: input_bytes.as_ptr().cast(),
|
|
size: input_bytes.len(),
|
|
pos: 0,
|
|
};
|
|
let output = ZSTDMT_streamOutputProjection::default();
|
|
let mut flush_called = false;
|
|
|
|
let result = unsafe {
|
|
compress_stream_generic_with(
|
|
context,
|
|
input,
|
|
output,
|
|
ZSTD_E_CONTINUE,
|
|
|_projection| panic!("input range should already be available"),
|
|
|_src_size, _end| ZSTDMT_streamCreateJobResult {
|
|
result: ERROR(ZstdErrorCode::DstSizeTooSmall),
|
|
inBuffActive: 1,
|
|
},
|
|
|_dst, _size, output_pos, _block_to_flush, _end| {
|
|
flush_called = true;
|
|
ZSTDMT_streamFlushResult {
|
|
result: 0,
|
|
outputPos: output_pos,
|
|
}
|
|
},
|
|
|_context, input, projection| ZSTDMT_syncPointProjection {
|
|
toLoad: (input.size - input.pos)
|
|
.min(round_buffer.len() - projection.bufferFilled),
|
|
flush: 0,
|
|
},
|
|
)
|
|
};
|
|
|
|
assert_eq!(result.result, ERROR(ZstdErrorCode::DstSizeTooSmall));
|
|
assert_eq!(result.inputPos, round_buffer.len());
|
|
assert_eq!(result.outputPos, 0);
|
|
assert_eq!(result.inBuffFilled, round_buffer.len());
|
|
assert!(!flush_called);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_last_block_serializes_after_buffer_callback() {
|
|
let mut storage = vec![0xa5u8; 3];
|
|
let expected_start = storage.as_mut_ptr().cast::<c_void>();
|
|
let mut callback_calls = 0;
|
|
let projection = ZSTDMT_emptyBlockJobProjection {
|
|
lastJob: 1,
|
|
srcSize: 0,
|
|
firstJob: 0,
|
|
dstStart: ptr::null_mut(),
|
|
dstCapacity: 0,
|
|
consumed: 0,
|
|
};
|
|
|
|
let result = unsafe {
|
|
write_last_empty_block_with(projection, || {
|
|
callback_calls += 1;
|
|
ZstdMtBuffer {
|
|
start: expected_start,
|
|
capacity: storage.len(),
|
|
}
|
|
})
|
|
};
|
|
|
|
assert_eq!(callback_calls, 1);
|
|
assert_eq!(result.buffer.start, expected_start);
|
|
assert_eq!(result.buffer.capacity, storage.len());
|
|
assert_eq!(result.cSize, 3);
|
|
assert_eq!(result.clearSource, 1);
|
|
assert_eq!(storage, [1, 0, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_last_block_keeps_source_on_buffer_allocation_failure() {
|
|
let projection = ZSTDMT_emptyBlockJobProjection {
|
|
lastJob: 1,
|
|
srcSize: 0,
|
|
firstJob: 0,
|
|
dstStart: ptr::null_mut(),
|
|
dstCapacity: 0,
|
|
consumed: 0,
|
|
};
|
|
let result = unsafe { write_last_empty_block_with(projection, ZstdMtBuffer::default) };
|
|
|
|
assert!(result.buffer.start.is_null());
|
|
assert_eq!(result.buffer.capacity, 0);
|
|
assert_eq!(result.cSize, ERROR(ZstdErrorCode::MemoryAllocation));
|
|
assert_eq!(result.clearSource, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_begin_uses_cdict_and_publishes_header_after_init() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let projection = ZSTDMT_compressionJobBeginProjection {
|
|
firstJob: 1,
|
|
hasCDict: 1,
|
|
};
|
|
|
|
let cdict_events = Rc::clone(&events);
|
|
let header_events = Rc::clone(&events);
|
|
let result = begin_compression_job_with(
|
|
projection,
|
|
move || {
|
|
cdict_events.borrow_mut().push("cdict");
|
|
0
|
|
},
|
|
|_| panic!("cdict jobs must not update force-window parameters"),
|
|
|| panic!("cdict jobs must not update deterministic-prefix parameters"),
|
|
|| panic!("cdict jobs must not initialize from a raw prefix"),
|
|
move || header_events.borrow_mut().push("header"),
|
|
);
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(&*events.borrow(), &["cdict", "header"]);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_begin_orders_nonfirst_parameter_policy_before_prefix_init() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let projection = ZSTDMT_compressionJobBeginProjection {
|
|
firstJob: 0,
|
|
hasCDict: 0,
|
|
};
|
|
|
|
let force_events = Rc::clone(&events);
|
|
let deterministic_events = Rc::clone(&events);
|
|
let prefix_events = Rc::clone(&events);
|
|
let header_events = Rc::clone(&events);
|
|
let result = begin_compression_job_with(
|
|
projection,
|
|
|| panic!("raw-prefix jobs must not use a cdict"),
|
|
move |value| {
|
|
assert_eq!(value, 1);
|
|
force_events.borrow_mut().push("force-window");
|
|
0
|
|
},
|
|
move || {
|
|
deterministic_events
|
|
.borrow_mut()
|
|
.push("deterministic-prefix");
|
|
0
|
|
},
|
|
move || {
|
|
prefix_events.borrow_mut().push("prefix");
|
|
0
|
|
},
|
|
move || header_events.borrow_mut().push("header"),
|
|
);
|
|
|
|
assert_eq!(result, 0);
|
|
assert_eq!(
|
|
&*events.borrow(),
|
|
&["force-window", "deterministic-prefix", "prefix", "header"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_begin_stops_after_force_window_error() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let expected_error = ERROR(ZstdErrorCode::MemoryAllocation);
|
|
let projection = ZSTDMT_compressionJobBeginProjection {
|
|
firstJob: 0,
|
|
hasCDict: 0,
|
|
};
|
|
let force_events = Rc::clone(&events);
|
|
|
|
let result = begin_compression_job_with(
|
|
projection,
|
|
|| panic!("parameter failure must stop before cdict initialization"),
|
|
move |_| {
|
|
force_events.borrow_mut().push("force-window");
|
|
expected_error
|
|
},
|
|
|| panic!("parameter failure must stop before deterministic-prefix update"),
|
|
|| panic!("parameter failure must stop before prefix initialization"),
|
|
|| panic!("parameter failure must stop before header publication"),
|
|
);
|
|
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(&*events.borrow(), &["force-window"]);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_begin_does_not_publish_header_after_init_error() {
|
|
let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
|
|
let expected_error = ERROR(ZstdErrorCode::Generic);
|
|
let projection = ZSTDMT_compressionJobBeginProjection {
|
|
firstJob: 1,
|
|
hasCDict: 1,
|
|
};
|
|
let cdict_events = Rc::clone(&events);
|
|
|
|
let result = begin_compression_job_with(
|
|
projection,
|
|
move || {
|
|
cdict_events.borrow_mut().push("cdict");
|
|
expected_error
|
|
},
|
|
|_| panic!("cdict initialization failure must not update parameters"),
|
|
|| panic!("cdict initialization failure must not update prefix policy"),
|
|
|| panic!("cdict initialization failure must not initialize a raw prefix"),
|
|
|| panic!("cdict initialization failure must not publish a header"),
|
|
);
|
|
|
|
assert_eq!(result, expected_error);
|
|
assert_eq!(&*events.borrow(), &["cdict"]);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_begin_rejects_cdict_on_nonfirst_job() {
|
|
let projection = ZSTDMT_compressionJobBeginProjection {
|
|
firstJob: 0,
|
|
hasCDict: 1,
|
|
};
|
|
|
|
let result = begin_compression_job_with(
|
|
projection,
|
|
|| panic!("invalid cdict placement must not initialize"),
|
|
|_| panic!("invalid cdict placement must not update parameters"),
|
|
|| panic!("invalid cdict placement must not update prefix policy"),
|
|
|| panic!("invalid cdict placement must not initialize a raw prefix"),
|
|
|| panic!("invalid cdict placement must not publish a header"),
|
|
);
|
|
|
|
assert_eq!(result, ERROR(ZstdErrorCode::StageWrong));
|
|
}
|
|
|
|
#[test]
|
|
fn chunk_loop_handles_empty_jobs_without_compression() {
|
|
let mut compressor = MockChunkCompressor {
|
|
continue_inputs: Vec::new(),
|
|
end_inputs: Vec::new(),
|
|
continue_results: Vec::new(),
|
|
end_results: vec![9],
|
|
};
|
|
let mut progress = MockChunkProgress::default();
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 0, 8, 0);
|
|
assert_eq!(result, ZSTDMT_chunkProcessResult::default());
|
|
assert!(compressor.continue_inputs.is_empty());
|
|
assert!(compressor.end_inputs.is_empty());
|
|
assert!(progress.calls.is_empty());
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 0, 8, 1);
|
|
assert_eq!(result.error, 0);
|
|
assert_eq!(result.lastBlockSize, 9);
|
|
assert!(compressor.continue_inputs.is_empty());
|
|
assert_eq!(compressor.end_inputs, [0]);
|
|
assert!(progress.calls.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn chunk_loop_handles_one_exact_chunk_and_last_job_dispatch() {
|
|
let mut compressor = MockChunkCompressor {
|
|
continue_inputs: Vec::new(),
|
|
end_inputs: Vec::new(),
|
|
continue_results: vec![4],
|
|
end_results: vec![6],
|
|
};
|
|
let mut progress = MockChunkProgress::default();
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 8, 8, 0);
|
|
assert_eq!(result.lastBlockSize, 4);
|
|
assert_eq!(compressor.continue_inputs, [8]);
|
|
assert!(compressor.end_inputs.is_empty());
|
|
assert!(progress.calls.is_empty());
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 8, 8, 1);
|
|
assert_eq!(result.lastBlockSize, 6);
|
|
assert_eq!(compressor.end_inputs, [8]);
|
|
assert!(progress.calls.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn chunk_loop_handles_multiple_exact_chunks_and_orders_progress() {
|
|
let mut compressor = MockChunkCompressor {
|
|
continue_inputs: Vec::new(),
|
|
end_inputs: Vec::new(),
|
|
continue_results: vec![2, 3],
|
|
end_results: Vec::new(),
|
|
};
|
|
let mut progress = MockChunkProgress::default();
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 16, 8, 0);
|
|
assert_eq!(result.lastBlockSize, 3);
|
|
assert_eq!(compressor.continue_inputs, [8, 8]);
|
|
assert!(compressor.end_inputs.is_empty());
|
|
assert_eq!(progress.calls, [(2, 8)]);
|
|
}
|
|
|
|
#[test]
|
|
fn chunk_loop_handles_partial_tail_and_final_dispatch() {
|
|
let mut compressor = MockChunkCompressor {
|
|
continue_inputs: Vec::new(),
|
|
end_inputs: Vec::new(),
|
|
continue_results: vec![2],
|
|
end_results: vec![5],
|
|
};
|
|
let mut progress = MockChunkProgress::default();
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 11, 8, 1);
|
|
assert_eq!(result.lastBlockSize, 5);
|
|
assert_eq!(compressor.continue_inputs, [8]);
|
|
assert_eq!(compressor.end_inputs, [3]);
|
|
assert_eq!(progress.calls, [(2, 8)]);
|
|
}
|
|
|
|
#[test]
|
|
fn chunk_loop_propagates_intermediate_errors_after_prior_progress() {
|
|
let error = ERROR(ZstdErrorCode::DstSizeTooSmall);
|
|
let mut compressor = MockChunkCompressor {
|
|
continue_inputs: Vec::new(),
|
|
end_inputs: Vec::new(),
|
|
continue_results: vec![2, error],
|
|
end_results: vec![7],
|
|
};
|
|
let mut progress = MockChunkProgress::default();
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 24, 8, 1);
|
|
assert_eq!(result.error, error);
|
|
assert_eq!(result.lastBlockSize, 0);
|
|
assert_eq!(compressor.continue_inputs, [8, 8]);
|
|
assert!(compressor.end_inputs.is_empty());
|
|
assert_eq!(progress.calls, [(2, 8)]);
|
|
}
|
|
|
|
#[test]
|
|
fn chunk_loop_propagates_final_errors_without_progress_callback() {
|
|
let error = ERROR(ZstdErrorCode::Generic);
|
|
let mut compressor = MockChunkCompressor {
|
|
continue_inputs: Vec::new(),
|
|
end_inputs: Vec::new(),
|
|
continue_results: Vec::new(),
|
|
end_results: vec![error],
|
|
};
|
|
let mut progress = MockChunkProgress::default();
|
|
|
|
let result = run_mock_chunk_loop(&mut compressor, &mut progress, 3, 8, 1);
|
|
assert_eq!(result.error, error);
|
|
assert_eq!(result.lastBlockSize, 0);
|
|
assert!(compressor.continue_inputs.is_empty());
|
|
assert_eq!(compressor.end_inputs, [3]);
|
|
assert!(progress.calls.is_empty());
|
|
}
|
|
|
|
unsafe extern "C" fn probe_job_table_init(
|
|
_job_table: *mut c_void,
|
|
_nb_jobs: c_uint,
|
|
_job_size: usize,
|
|
) -> c_int {
|
|
JOB_TABLE_INIT_CALLS.fetch_add(1, Ordering::Relaxed);
|
|
if JOB_TABLE_FAIL_INIT.load(Ordering::Relaxed) {
|
|
-1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn probe_job_table_destroy(
|
|
_job_table: *mut c_void,
|
|
_nb_jobs: c_uint,
|
|
_job_size: usize,
|
|
) {
|
|
JOB_TABLE_DESTROY_CALLS.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct FreeJobsTableTestContext {
|
|
events: Vec<&'static str>,
|
|
}
|
|
|
|
unsafe extern "C" fn free_jobs_table_test_alloc(
|
|
_opaque: *mut c_void,
|
|
_size: usize,
|
|
) -> *mut c_void {
|
|
ptr::null_mut()
|
|
}
|
|
|
|
unsafe extern "C" fn free_jobs_table_test_destroy(
|
|
job_table: *mut c_void,
|
|
_nb_jobs: c_uint,
|
|
_job_size: usize,
|
|
) {
|
|
unsafe {
|
|
(*job_table.cast::<FreeJobsTableTestContext>())
|
|
.events
|
|
.push("destroy-sync");
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn free_jobs_table_test_free(opaque: *mut c_void, allocation: *mut c_void) {
|
|
assert_eq!(allocation, opaque);
|
|
unsafe {
|
|
(*opaque.cast::<FreeJobsTableTestContext>())
|
|
.events
|
|
.push("free-storage");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn free_jobs_table_orders_sync_destruction_before_storage_release() {
|
|
let mut context = FreeJobsTableTestContext::default();
|
|
let context_ptr = (&mut context as *mut FreeJobsTableTestContext).cast::<c_void>();
|
|
let projection = ZSTDMT_RustFreeJobsTableProjection {
|
|
jobTable: context_ptr,
|
|
nbJobs: 8,
|
|
jobSize: 128,
|
|
customMem: ZstdCustomMem {
|
|
customAlloc: Some(free_jobs_table_test_alloc),
|
|
customFree: Some(free_jobs_table_test_free),
|
|
opaque: context_ptr,
|
|
},
|
|
destroySync: Some(free_jobs_table_test_destroy),
|
|
};
|
|
|
|
unsafe { ZSTDMT_rust_freeJobsTable(&projection) };
|
|
|
|
assert_eq!(context.events, ["destroy-sync", "free-storage"]);
|
|
}
|
|
|
|
fn rsync_prime_power() -> u64 {
|
|
(0..RSYNC_LENGTH - 1).fold(1, |power, _| power.wrapping_mul(PRIME8_BYTES))
|
|
}
|
|
|
|
fn patterned_bytes(length: usize, seed: u8) -> Vec<u8> {
|
|
let mut value = seed;
|
|
let mut bytes = vec![0; length];
|
|
for byte in &mut bytes {
|
|
value = value.wrapping_mul(17).wrapping_add(29);
|
|
*byte = value;
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn rolling_hash_bytes(bytes: &[u8]) -> u64 {
|
|
bytes.iter().fold(0, |hash, &byte| {
|
|
hash.wrapping_mul(PRIME8_BYTES)
|
|
.wrapping_add(u64::from(byte).wrapping_add(ROLL_HASH_CHAR_OFFSET))
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn rolling_hash_prime_power_matches_c_ipow_for_common_lengths() {
|
|
assert_eq!(ZSTDMT_rust_rollingHashPrimePower(1), 0x0000_0000_0000_0001);
|
|
assert_eq!(ZSTDMT_rust_rollingHashPrimePower(2), 0xCF1B_BCDC_B7A5_6463);
|
|
assert_eq!(
|
|
ZSTDMT_rust_rollingHashPrimePower(RSYNC_LENGTH as c_uint),
|
|
0xF550_7FE3_5F91_F8CB
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rolling_hash_prime_power_preserves_zero_length_underflow() {
|
|
assert_eq!(0u32.wrapping_sub(1), c_uint::MAX);
|
|
assert_eq!(ZSTDMT_rust_rollingHashPrimePower(0), 0x12A9_3A33_31E0_3D4B);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_progression_zero_jobs_keeps_mt_base_values() {
|
|
assert_eq!(
|
|
ZSTDMT_rust_frameProgression(11, 7, 13, 4),
|
|
ZSTD_frameProgression {
|
|
ingested: 18,
|
|
consumed: 11,
|
|
produced: 13,
|
|
flushed: 13,
|
|
currentJobID: 4,
|
|
nbActiveWorkers: 0,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_progression_one_ready_job_counts_as_active() {
|
|
let base = ZSTDMT_rust_frameProgression(100, 20, 300, 9);
|
|
assert_eq!(
|
|
ZSTDMT_rust_frameProgressionAddJob(base, 80, 17, 42, 10),
|
|
ZSTD_frameProgression {
|
|
ingested: 200,
|
|
consumed: 117,
|
|
produced: 342,
|
|
flushed: 310,
|
|
currentJobID: 9,
|
|
nbActiveWorkers: 1,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_progression_completed_job_is_not_active() {
|
|
let base = ZSTDMT_rust_frameProgression(100, 20, 300, 9);
|
|
let progression = ZSTDMT_rust_frameProgressionAddJob(base, 80, 80, 42, 42);
|
|
|
|
assert_eq!(progression.nbActiveWorkers, 0);
|
|
assert_eq!(progression.ingested, 200);
|
|
assert_eq!(progression.consumed, 180);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_progression_error_job_uses_normalized_zero_output() {
|
|
let base = ZSTDMT_rust_frameProgression(100, 20, 300, 9);
|
|
let progression = ZSTDMT_rust_frameProgressionAddJob(base, 80, 0, 0, 0);
|
|
|
|
assert_eq!(progression.produced, 300);
|
|
assert_eq!(progression.flushed, 300);
|
|
assert_eq!(progression.nbActiveWorkers, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_progression_aggregates_active_and_completed_jobs() {
|
|
let base = ZSTDMT_rust_frameProgression(100, 20, 300, 9);
|
|
let progression = ZSTDMT_rust_frameProgressionAddJob(base, 80, 17, 42, 10);
|
|
let progression = ZSTDMT_rust_frameProgressionAddJob(progression, 30, 30, 8, 8);
|
|
let progression = ZSTDMT_rust_frameProgressionAddJob(progression, 40, 5, 7, 6);
|
|
|
|
assert_eq!(
|
|
progression,
|
|
ZSTD_frameProgression {
|
|
ingested: 270,
|
|
consumed: 152,
|
|
produced: 357,
|
|
flushed: 324,
|
|
currentJobID: 9,
|
|
nbActiveWorkers: 2,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_progression_with_jobs_masks_slots_and_normalizes_errors() {
|
|
let projections = [
|
|
ZSTDMT_jobProjection {
|
|
consumed: 50,
|
|
cSize: 8,
|
|
srcSize: 50,
|
|
dstFlushed: 8,
|
|
..ZSTDMT_jobProjection::default()
|
|
},
|
|
ZSTDMT_jobProjection {
|
|
consumed: 17,
|
|
cSize: 42,
|
|
srcSize: 80,
|
|
dstFlushed: 10,
|
|
..ZSTDMT_jobProjection::default()
|
|
},
|
|
ZSTDMT_jobProjection {
|
|
consumed: 30,
|
|
cSize: ERROR(ZstdErrorCode::SequenceProducerFailed),
|
|
srcSize: 30,
|
|
dstFlushed: 29,
|
|
..ZSTDMT_jobProjection::default()
|
|
},
|
|
ZSTDMT_jobProjection {
|
|
consumed: 5,
|
|
cSize: 7,
|
|
srcSize: 40,
|
|
dstFlushed: 6,
|
|
..ZSTDMT_jobProjection::default()
|
|
},
|
|
];
|
|
let mut calls = Vec::new();
|
|
|
|
let progression = frame_progression_with_jobs(100, 20, 300, 9, 5, 8, 1, 3, |job_id| {
|
|
calls.push(job_id);
|
|
projections[job_id as usize]
|
|
});
|
|
|
|
assert_eq!(calls, [1, 2, 3, 0]);
|
|
assert_eq!(
|
|
progression,
|
|
ZSTD_frameProgression {
|
|
ingested: 320,
|
|
consumed: 202,
|
|
produced: 357,
|
|
flushed: 324,
|
|
currentJobID: 9,
|
|
nbActiveWorkers: 2,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn input_data_in_use_skips_first_round_and_scans_oldest_ring_slot() {
|
|
let source_a = [0u8; 8];
|
|
let backing = [1u8; 16];
|
|
let source_b = &backing[8..];
|
|
let prefix_b = &backing[..3];
|
|
let projections = [
|
|
ZSTDMT_jobProjection {
|
|
consumed: source_a.len(),
|
|
srcStart: source_a.as_ptr().cast(),
|
|
srcSize: source_a.len(),
|
|
..ZSTDMT_jobProjection::default()
|
|
},
|
|
ZSTDMT_jobProjection {
|
|
consumed: 2,
|
|
srcStart: source_b.as_ptr().cast(),
|
|
srcSize: source_b.len(),
|
|
prefixStart: prefix_b.as_ptr().cast(),
|
|
prefixSize: prefix_b.len(),
|
|
..ZSTDMT_jobProjection::default()
|
|
},
|
|
];
|
|
|
|
let mut calls = 0;
|
|
let result = get_input_data_in_use_with(4, 6, 1, 16, 8, |slot| {
|
|
calls += 1;
|
|
projections[slot as usize]
|
|
});
|
|
|
|
assert_eq!(calls, 2);
|
|
assert_eq!(result.start, prefix_b.as_ptr().cast());
|
|
assert_eq!(result.size, prefix_b.len());
|
|
|
|
let mut first_round_calls = 0;
|
|
let result = get_input_data_in_use_with(0, 3, 1, 32, 8, |slot| {
|
|
first_round_calls += 1;
|
|
projections[slot as usize]
|
|
});
|
|
assert_eq!(first_round_calls, 0);
|
|
assert_eq!(result, ZSTDMT_inputRange::default());
|
|
}
|
|
|
|
#[test]
|
|
fn input_data_in_use_falls_back_to_source_without_prefix() {
|
|
let source = [3u8; 11];
|
|
let projection = ZSTDMT_jobProjection {
|
|
consumed: 4,
|
|
srcStart: source.as_ptr().cast(),
|
|
srcSize: source.len(),
|
|
..ZSTDMT_jobProjection::default()
|
|
};
|
|
|
|
let result = get_input_data_in_use_with(7, 8, 7, 8, 8, |_| projection);
|
|
assert_eq!(result.start, source.as_ptr().cast());
|
|
assert_eq!(result.size, source.len());
|
|
}
|
|
|
|
#[test]
|
|
fn try_get_input_range_selects_next_section_without_wrapping() {
|
|
let round_buffer = [0u8; 32];
|
|
let prefix = [1u8; 4];
|
|
let projection = ZSTDMT_tryGetInputRangeProjection {
|
|
roundBufferStart: round_buffer.as_ptr().cast(),
|
|
roundBufferCapacity: round_buffer.len(),
|
|
roundBufferPos: 8,
|
|
prefixStart: prefix.as_ptr().cast(),
|
|
prefixSize: prefix.len(),
|
|
targetSectionSize: 8,
|
|
..ZSTDMT_tryGetInputRangeProjection::default()
|
|
};
|
|
|
|
let result = try_get_input_range_with(projection);
|
|
|
|
assert_eq!(result.ready, 1);
|
|
assert_eq!(result.movePrefix, 0);
|
|
assert_eq!(result.roundBufferPos, 8);
|
|
assert_eq!(
|
|
result.bufferStart,
|
|
round_buffer.as_ptr().wrapping_add(8).cast_mut().cast()
|
|
);
|
|
assert_eq!(result.bufferCapacity, 8);
|
|
}
|
|
|
|
#[test]
|
|
fn try_get_input_range_wraps_after_prefix_and_rejects_live_ranges() {
|
|
let round_buffer = [0u8; 32];
|
|
let prefix = [1u8; 4];
|
|
let wrapped = ZSTDMT_tryGetInputRangeProjection {
|
|
roundBufferStart: round_buffer.as_ptr().cast(),
|
|
roundBufferCapacity: round_buffer.len(),
|
|
roundBufferPos: 28,
|
|
prefixStart: prefix.as_ptr().cast(),
|
|
prefixSize: prefix.len(),
|
|
targetSectionSize: 8,
|
|
..ZSTDMT_tryGetInputRangeProjection::default()
|
|
};
|
|
let result = try_get_input_range_with(wrapped);
|
|
assert_eq!(result.ready, 1);
|
|
assert_eq!(result.movePrefix, 1);
|
|
assert_eq!(result.roundBufferPos, prefix.len());
|
|
assert_eq!(
|
|
result.bufferStart,
|
|
round_buffer.as_ptr().wrapping_add(4).cast_mut().cast()
|
|
);
|
|
|
|
let blocked_prefix = ZSTDMT_tryGetInputRangeProjection {
|
|
inUseStart: round_buffer.as_ptr().cast(),
|
|
inUseSize: prefix.len(),
|
|
..wrapped
|
|
};
|
|
assert_eq!(try_get_input_range_with(blocked_prefix).ready, 0);
|
|
|
|
let blocked_source = ZSTDMT_tryGetInputRangeProjection {
|
|
roundBufferPos: 8,
|
|
inUseStart: round_buffer.as_ptr().wrapping_add(8).cast(),
|
|
inUseSize: 8,
|
|
..wrapped
|
|
};
|
|
assert_eq!(try_get_input_range_with(blocked_source).ready, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn commit_input_range_wraps_prefix_before_waiting_for_source() {
|
|
let mut round_buffer = [0u8; 32];
|
|
let prefix = [1u8; 4];
|
|
let round_buffer_start = round_buffer.as_mut_ptr();
|
|
let selected_buffer = round_buffer_start.wrapping_add(prefix.len());
|
|
let result = ZSTDMT_tryGetInputRangeResult {
|
|
ready: 1,
|
|
movePrefix: 1,
|
|
bufferStart: selected_buffer.cast(),
|
|
bufferCapacity: 8,
|
|
roundBufferPos: prefix.len(),
|
|
};
|
|
let events = Rc::new(RefCell::new(Vec::new()));
|
|
let wait_events = Rc::clone(&events);
|
|
let move_events = Rc::clone(&events);
|
|
let publish_events = Rc::clone(&events);
|
|
let wait_calls = Rc::new(RefCell::new(Vec::new()));
|
|
let wait_call_records = Rc::clone(&wait_calls);
|
|
|
|
let committed = commit_input_range_with(
|
|
result,
|
|
round_buffer_start.cast(),
|
|
prefix.as_ptr().cast(),
|
|
prefix.len(),
|
|
ptr::null_mut(),
|
|
move |_, buffer_start, buffer_capacity| {
|
|
wait_call_records
|
|
.borrow_mut()
|
|
.push((buffer_start, buffer_capacity));
|
|
wait_events.borrow_mut().push("wait");
|
|
},
|
|
move |_, destination, source, size, round_buffer_pos| {
|
|
assert_eq!(destination, round_buffer_start.cast());
|
|
assert_eq!(source, prefix.as_ptr().cast());
|
|
assert_eq!(size, prefix.len());
|
|
assert_eq!(round_buffer_pos, prefix.len());
|
|
unsafe {
|
|
ptr::copy_nonoverlapping(source.cast::<u8>(), destination.cast::<u8>(), size);
|
|
}
|
|
move_events.borrow_mut().push("move");
|
|
},
|
|
move |_, buffer_start, buffer_capacity| {
|
|
assert_eq!(buffer_start, selected_buffer.cast());
|
|
assert_eq!(buffer_capacity, 8);
|
|
publish_events.borrow_mut().push("publish");
|
|
},
|
|
);
|
|
|
|
assert_eq!(committed, 1);
|
|
assert_eq!(&*events.borrow(), &["wait", "move", "wait", "publish"]);
|
|
assert_eq!(
|
|
&*wait_calls.borrow(),
|
|
&[
|
|
(round_buffer_start.cast(), prefix.len()),
|
|
(selected_buffer.cast(), 8),
|
|
]
|
|
);
|
|
assert_eq!(&round_buffer[..prefix.len()], &prefix);
|
|
}
|
|
|
|
#[test]
|
|
fn commit_input_range_without_wrap_waits_once_before_publish() {
|
|
let mut round_buffer = [0u8; 32];
|
|
let round_buffer_start = round_buffer.as_mut_ptr();
|
|
let selected_buffer = round_buffer_start.wrapping_add(8);
|
|
let result = ZSTDMT_tryGetInputRangeResult {
|
|
ready: 1,
|
|
bufferStart: selected_buffer.cast(),
|
|
bufferCapacity: 8,
|
|
roundBufferPos: 8,
|
|
..ZSTDMT_tryGetInputRangeResult::default()
|
|
};
|
|
let events = Rc::new(RefCell::new(Vec::new()));
|
|
let wait_events = Rc::clone(&events);
|
|
let move_called = Rc::new(RefCell::new(false));
|
|
let move_called_by_callback = Rc::clone(&move_called);
|
|
let publish_events = Rc::clone(&events);
|
|
|
|
let committed = commit_input_range_with(
|
|
result,
|
|
round_buffer_start.cast(),
|
|
ptr::null(),
|
|
0,
|
|
ptr::null_mut(),
|
|
move |_, buffer_start, buffer_capacity| {
|
|
assert_eq!(buffer_start, selected_buffer.cast());
|
|
assert_eq!(buffer_capacity, 8);
|
|
wait_events.borrow_mut().push("wait");
|
|
},
|
|
move |_, _, _, _, _| {
|
|
*move_called_by_callback.borrow_mut() = true;
|
|
},
|
|
move |_, buffer_start, buffer_capacity| {
|
|
assert_eq!(buffer_start, selected_buffer.cast());
|
|
assert_eq!(buffer_capacity, 8);
|
|
publish_events.borrow_mut().push("publish");
|
|
},
|
|
);
|
|
|
|
assert_eq!(committed, 1);
|
|
assert_eq!(&*events.borrow(), &["wait", "publish"]);
|
|
assert!(!*move_called.borrow());
|
|
}
|
|
|
|
#[test]
|
|
fn to_flush_now_projects_oldest_slot_and_normalizes_errors() {
|
|
let projection = ZSTDMT_jobProjection {
|
|
consumed: 5,
|
|
cSize: 23,
|
|
srcSize: 17,
|
|
dstFlushed: 9,
|
|
..ZSTDMT_jobProjection::default()
|
|
};
|
|
let mut seen_slot = None;
|
|
let to_flush = to_flush_now_with(5, 9, 3, |slot| {
|
|
seen_slot = Some(slot);
|
|
projection
|
|
});
|
|
assert_eq!(seen_slot, Some(1));
|
|
assert_eq!(to_flush, 14);
|
|
|
|
let error_projection = ZSTDMT_jobProjection {
|
|
consumed: 0,
|
|
cSize: ERROR(ZstdErrorCode::Generic),
|
|
srcSize: 17,
|
|
dstFlushed: 23,
|
|
..ZSTDMT_jobProjection::default()
|
|
};
|
|
assert_eq!(to_flush_now_with(1, 2, 1, |_| error_projection), 0);
|
|
|
|
let mut called = false;
|
|
assert_eq!(
|
|
to_flush_now_with(2, 2, 1, |_| {
|
|
called = true;
|
|
projection
|
|
}),
|
|
0
|
|
);
|
|
assert!(!called);
|
|
}
|
|
|
|
fn call_synchronization_point(
|
|
input: &[u8],
|
|
input_pos: usize,
|
|
in_buff: &[u8],
|
|
in_buff_filled: usize,
|
|
target_section_size: usize,
|
|
rsyncable: c_int,
|
|
hit_mask: u64,
|
|
) -> (usize, c_int) {
|
|
let mut to_load = usize::MAX;
|
|
let mut flush = -1;
|
|
unsafe {
|
|
ZSTDMT_rust_findSynchronizationPoint(
|
|
input.as_ptr().cast(),
|
|
input.len(),
|
|
input_pos,
|
|
target_section_size,
|
|
in_buff.as_ptr().cast(),
|
|
in_buff_filled,
|
|
rsyncable,
|
|
rsync_prime_power(),
|
|
hit_mask,
|
|
&mut to_load,
|
|
&mut flush,
|
|
);
|
|
}
|
|
(to_load, flush)
|
|
}
|
|
|
|
fn input_hashes_after_each_byte(
|
|
in_buff: &[u8],
|
|
in_buff_filled: usize,
|
|
input: &[u8],
|
|
input_pos: usize,
|
|
) -> (u64, Vec<u64>) {
|
|
let mut history = in_buff[in_buff_filled - RSYNC_LENGTH..in_buff_filled].to_vec();
|
|
history.extend_from_slice(&input[input_pos..]);
|
|
let initial = rolling_hash_bytes(&history[..RSYNC_LENGTH]);
|
|
let hashes = (0..input.len() - input_pos)
|
|
.map(|pos| rolling_hash_bytes(&history[pos + 1..pos + 1 + RSYNC_LENGTH]))
|
|
.collect();
|
|
(initial, hashes)
|
|
}
|
|
|
|
fn hit_mask_for_index(initial: u64, hashes: &[u64], target: usize) -> u64 {
|
|
let target_hash = hashes[target];
|
|
let prior_matches = |mask: u64| {
|
|
(initial & mask) == mask || hashes[..target].iter().any(|hash| (hash & mask) == mask)
|
|
};
|
|
if target_hash != 0 && !prior_matches(target_hash) {
|
|
return target_hash;
|
|
}
|
|
for mask in 1..=u64::from(u16::MAX) {
|
|
if target_hash & mask == mask && !prior_matches(mask) {
|
|
return mask;
|
|
}
|
|
}
|
|
panic!("could not isolate synchronization hash at position {target}");
|
|
}
|
|
|
|
#[test]
|
|
fn synchronization_point_keeps_rsync_disabled() {
|
|
let input = patterned_bytes(20, 3);
|
|
let in_buff = patterned_bytes(64, 5);
|
|
|
|
assert_eq!(
|
|
call_synchronization_point(&input, 3, &in_buff, 40, 100, 0, 0),
|
|
(17, 0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn synchronization_point_does_not_scan_a_short_hash_window() {
|
|
let input = patterned_bytes(RSYNC_LENGTH - 1, 7);
|
|
let in_buff = patterned_bytes(RSYNC_MIN_BLOCK_SIZE, 11);
|
|
|
|
assert_eq!(
|
|
call_synchronization_point(
|
|
&input,
|
|
0,
|
|
&in_buff,
|
|
0,
|
|
RSYNC_MIN_BLOCK_SIZE + input.len(),
|
|
1,
|
|
0,
|
|
),
|
|
(input.len(), 0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn synchronization_point_respects_min_block_boundaries() {
|
|
let in_buff = patterned_bytes(RSYNC_MIN_BLOCK_SIZE, 13);
|
|
for &in_buff_filled in &[
|
|
0,
|
|
RSYNC_LENGTH - 1,
|
|
RSYNC_LENGTH,
|
|
RSYNC_MIN_BLOCK_SIZE - 1,
|
|
RSYNC_MIN_BLOCK_SIZE,
|
|
] {
|
|
let input_len = if in_buff_filled < RSYNC_MIN_BLOCK_SIZE {
|
|
RSYNC_MIN_BLOCK_SIZE - in_buff_filled + 1
|
|
} else {
|
|
1
|
|
};
|
|
let input = patterned_bytes(input_len, in_buff_filled as u8);
|
|
let expected = if in_buff_filled == RSYNC_MIN_BLOCK_SIZE {
|
|
(0, 1)
|
|
} else {
|
|
(input_len, 1)
|
|
};
|
|
|
|
assert_eq!(
|
|
call_synchronization_point(
|
|
&input,
|
|
0,
|
|
&in_buff,
|
|
in_buff_filled,
|
|
in_buff_filled + input_len,
|
|
1,
|
|
0,
|
|
),
|
|
expected,
|
|
"inBuff.filled={in_buff_filled}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn synchronization_point_reports_initial_first_middle_and_final_hits() {
|
|
let in_buff = patterned_bytes(RSYNC_MIN_BLOCK_SIZE, 17);
|
|
let input = patterned_bytes(96, 19);
|
|
let (initial, hashes) =
|
|
input_hashes_after_each_byte(&in_buff, RSYNC_MIN_BLOCK_SIZE, &input, 0);
|
|
|
|
assert_eq!(
|
|
call_synchronization_point(
|
|
&input,
|
|
0,
|
|
&in_buff,
|
|
RSYNC_MIN_BLOCK_SIZE,
|
|
RSYNC_MIN_BLOCK_SIZE + input.len(),
|
|
1,
|
|
0,
|
|
),
|
|
(0, 1)
|
|
);
|
|
|
|
for &(name, index) in &[("first", 0), ("middle", 37), ("final", 95)] {
|
|
let hit_mask = hit_mask_for_index(initial, &hashes, index);
|
|
assert_eq!(
|
|
call_synchronization_point(
|
|
&input,
|
|
0,
|
|
&in_buff,
|
|
RSYNC_MIN_BLOCK_SIZE,
|
|
RSYNC_MIN_BLOCK_SIZE + input.len(),
|
|
1,
|
|
hit_mask,
|
|
),
|
|
(index + 1, 1),
|
|
"{name} hit"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn synchronization_point_loads_all_input_when_no_hash_hits() {
|
|
let in_buff = patterned_bytes(RSYNC_MIN_BLOCK_SIZE, 23);
|
|
let input = patterned_bytes(96, 29);
|
|
let (initial, hashes) =
|
|
input_hashes_after_each_byte(&in_buff, RSYNC_MIN_BLOCK_SIZE, &input, 0);
|
|
assert_ne!(initial, u64::MAX);
|
|
assert!(hashes.iter().all(|&hash| hash != u64::MAX));
|
|
|
|
assert_eq!(
|
|
call_synchronization_point(
|
|
&input,
|
|
0,
|
|
&in_buff,
|
|
RSYNC_MIN_BLOCK_SIZE,
|
|
RSYNC_MIN_BLOCK_SIZE + input.len(),
|
|
1,
|
|
u64::MAX,
|
|
),
|
|
(input.len(), 0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn synchronization_point_preserves_nonzero_input_position() {
|
|
let in_buff = patterned_bytes(RSYNC_MIN_BLOCK_SIZE, 31);
|
|
let prefix = patterned_bytes(7, 37);
|
|
let payload = patterned_bytes(64, 41);
|
|
let mut input = prefix;
|
|
input.extend_from_slice(&payload);
|
|
let (initial, hashes) =
|
|
input_hashes_after_each_byte(&in_buff, RSYNC_MIN_BLOCK_SIZE, &input, 7);
|
|
let hit_mask = hit_mask_for_index(initial, &hashes, 23);
|
|
|
|
assert_eq!(
|
|
call_synchronization_point(
|
|
&input,
|
|
7,
|
|
&in_buff,
|
|
RSYNC_MIN_BLOCK_SIZE,
|
|
RSYNC_MIN_BLOCK_SIZE + payload.len(),
|
|
1,
|
|
hit_mask,
|
|
),
|
|
(24, 1)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn synchronization_point_handles_a_window_spanning_the_buffer_boundary() {
|
|
let in_buff = patterned_bytes(RSYNC_MIN_BLOCK_SIZE, 43);
|
|
let input = [0xe1, 0x7a];
|
|
let in_buff_filled = RSYNC_MIN_BLOCK_SIZE - 1;
|
|
let mut history = in_buff[in_buff_filled - RSYNC_LENGTH..in_buff_filled].to_vec();
|
|
history.extend_from_slice(&input);
|
|
let hit_mask = rolling_hash_bytes(&history[2..2 + RSYNC_LENGTH]);
|
|
|
|
assert_eq!(
|
|
call_synchronization_point(
|
|
&input,
|
|
0,
|
|
&in_buff,
|
|
in_buff_filled,
|
|
in_buff_filled + input.len(),
|
|
1,
|
|
hit_mask,
|
|
),
|
|
(input.len(), 1)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn raw_seq_buffer_conversion_uses_whole_element_capacity() {
|
|
let mut sequences = [ZstdMtRawSeq::default(); 3];
|
|
let element_size = mem::size_of::<ZstdMtRawSeq>();
|
|
let buffer = ZstdMtBuffer {
|
|
start: sequences.as_mut_ptr().cast(),
|
|
capacity: element_size * sequences.len() + element_size - 1,
|
|
};
|
|
|
|
let seq = ZSTDMT_rust_bufferToSeq(buffer);
|
|
assert_eq!(seq.seq, sequences.as_mut_ptr());
|
|
assert_eq!(seq.pos, 0);
|
|
assert_eq!(seq.posInSequence, 0);
|
|
assert_eq!(seq.size, 0);
|
|
assert_eq!(seq.capacity, sequences.len());
|
|
|
|
let roundtrip = ZSTDMT_rust_seqToBuffer(seq);
|
|
assert_eq!(roundtrip.start, buffer.start);
|
|
assert_eq!(roundtrip.capacity, element_size * sequences.len());
|
|
}
|
|
|
|
#[test]
|
|
fn raw_seq_to_buffer_preserves_pointer_and_size_t_multiplication() {
|
|
let seq = ZstdMtRawSeqStore {
|
|
seq: ptr::null_mut(),
|
|
pos: 4,
|
|
posInSequence: 5,
|
|
size: 6,
|
|
capacity: usize::MAX,
|
|
};
|
|
let buffer = ZSTDMT_rust_seqToBuffer(seq);
|
|
assert!(buffer.start.is_null());
|
|
assert_eq!(
|
|
buffer.capacity,
|
|
usize::MAX.wrapping_mul(mem::size_of::<ZstdMtRawSeq>())
|
|
);
|
|
|
|
let empty = ZSTDMT_rust_bufferToSeq(ZstdMtBuffer {
|
|
start: ptr::null_mut(),
|
|
capacity: mem::size_of::<ZstdMtRawSeq>() - 1,
|
|
});
|
|
assert!(empty.seq.is_null());
|
|
assert_eq!(empty.capacity, 0);
|
|
assert_eq!(empty.pos, 0);
|
|
assert_eq!(empty.posInSequence, 0);
|
|
assert_eq!(empty.size, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn overlapped_rejects_null_ranges() {
|
|
let bytes = [0u8; 8];
|
|
let start = bytes.as_ptr().cast::<c_void>();
|
|
|
|
assert_eq!(ZSTDMT_rust_isOverlapped(ptr::null(), 4, start, 4), 0);
|
|
assert_eq!(ZSTDMT_rust_isOverlapped(start, 4, ptr::null(), 4), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn overlapped_rejects_empty_ranges() {
|
|
let bytes = [0u8; 8];
|
|
let start = bytes.as_ptr().cast::<c_void>();
|
|
|
|
assert_eq!(ZSTDMT_rust_isOverlapped(start, 0, start, 4), 0);
|
|
assert_eq!(ZSTDMT_rust_isOverlapped(start, 4, start, 0), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn overlapped_uses_half_open_range_boundaries() {
|
|
let bytes = [0u8; 16];
|
|
let start = bytes.as_ptr();
|
|
let middle = start.wrapping_add(4);
|
|
|
|
assert_eq!(
|
|
ZSTDMT_rust_isOverlapped(start.cast(), 4, middle.cast(), 4),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_isOverlapped(middle.cast(), 4, start.cast(), 4),
|
|
0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn overlapped_detects_contained_ranges() {
|
|
let bytes = [0u8; 16];
|
|
let start = bytes.as_ptr();
|
|
let inner = start.wrapping_add(4);
|
|
|
|
assert_eq!(
|
|
ZSTDMT_rust_isOverlapped(start.cast(), 12, inner.cast(), 4),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_isOverlapped(inner.cast(), 4, start.cast(), 12),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn overlapped_rejects_disjoint_ranges() {
|
|
let bytes = [0u8; 16];
|
|
let start = bytes.as_ptr();
|
|
let after = start.wrapping_add(8);
|
|
|
|
assert_eq!(
|
|
ZSTDMT_rust_isOverlapped(start.cast(), 4, after.cast(), 4),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_isOverlapped(after.cast(), 4, start.cast(), 4),
|
|
0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn overlap_window_checks_external_dictionary_and_prefix() {
|
|
let bytes = [0u8; 32];
|
|
let base = bytes.as_ptr();
|
|
let next_src = base.wrapping_add(16);
|
|
let dict_base = base.wrapping_add(16);
|
|
|
|
assert_eq!(
|
|
ZSTDMT_rust_doesOverlapWindow(
|
|
base.wrapping_add(20).cast(),
|
|
4,
|
|
next_src.cast(),
|
|
base.cast(),
|
|
dict_base.cast(),
|
|
8,
|
|
4,
|
|
),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_doesOverlapWindow(
|
|
base.wrapping_add(12).cast(),
|
|
4,
|
|
next_src.cast(),
|
|
base.cast(),
|
|
dict_base.cast(),
|
|
8,
|
|
4,
|
|
),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
ZSTDMT_rust_doesOverlapWindow(
|
|
base.cast(),
|
|
4,
|
|
next_src.cast(),
|
|
base.cast(),
|
|
dict_base.cast(),
|
|
8,
|
|
4,
|
|
),
|
|
0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn direct_overlap_window_abi_matches_scalar_policy() {
|
|
let bytes = [0u8; 32];
|
|
let base = bytes.as_ptr();
|
|
let window = ZstdMtWindow {
|
|
nextSrc: base.wrapping_add(16),
|
|
base,
|
|
dictBase: base.wrapping_add(16),
|
|
dictLimit: 8,
|
|
lowLimit: 4,
|
|
nbOverflowCorrections: 0,
|
|
};
|
|
let buffer = ZstdMtBuffer {
|
|
start: base.wrapping_add(20) as *mut c_void,
|
|
capacity: 4,
|
|
};
|
|
assert_eq!(ZSTDMT_doesOverlapWindow(buffer, window), 1);
|
|
assert_eq!(
|
|
ZSTDMT_doesOverlapWindow(
|
|
ZstdMtBuffer {
|
|
start: base as *mut u8 as *mut c_void,
|
|
capacity: 4,
|
|
},
|
|
window,
|
|
),
|
|
0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn buffer_pool_reuses_and_resizes_buffers() {
|
|
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(2, DEFAULT_MEM) };
|
|
assert!(!pool.is_null());
|
|
|
|
let first = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
|
|
assert!(!first.start.is_null());
|
|
assert_eq!(first.capacity, 64 << 10);
|
|
unsafe { ZSTDMT_rust_buffer_pool_release(pool, first) };
|
|
|
|
let reused = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
|
|
assert_eq!(reused.start, first.start);
|
|
assert_eq!(reused.capacity, first.capacity);
|
|
unsafe { ZSTDMT_rust_buffer_pool_release(pool, reused) };
|
|
|
|
unsafe { ZSTDMT_rust_buffer_pool_set_size(pool, 128 << 10) };
|
|
let resized = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
|
|
assert_eq!(resized.capacity, 128 << 10);
|
|
unsafe {
|
|
ZSTDMT_rust_buffer_pool_release(pool, resized);
|
|
ZSTDMT_rust_buffer_pool_free(pool);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zero_size_pool_does_not_allocate_sequence_storage() {
|
|
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(1, DEFAULT_MEM) };
|
|
assert!(!pool.is_null());
|
|
unsafe { ZSTDMT_rust_buffer_pool_set_size(pool, 0) };
|
|
let buffer = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
|
|
assert!(buffer.start.is_null());
|
|
assert_eq!(buffer.capacity, 0);
|
|
unsafe { ZSTDMT_rust_buffer_pool_free(pool) };
|
|
}
|
|
|
|
#[test]
|
|
fn expansion_preserves_requested_buffer_size() {
|
|
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(1, DEFAULT_MEM) };
|
|
assert!(!pool.is_null());
|
|
unsafe { ZSTDMT_rust_buffer_pool_set_size(pool, 96 << 10) };
|
|
let expanded = unsafe { ZSTDMT_rust_buffer_pool_expand(pool, 3) };
|
|
assert!(!expanded.is_null());
|
|
let buffer = unsafe { ZSTDMT_rust_buffer_pool_get(expanded) };
|
|
assert_eq!(buffer.capacity, 96 << 10);
|
|
unsafe {
|
|
ZSTDMT_rust_buffer_pool_release(expanded, buffer);
|
|
ZSTDMT_rust_buffer_pool_free(expanded);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resize_preserves_existing_buffer_contents() {
|
|
let pool = unsafe { ZSTDMT_rust_buffer_pool_create(1, DEFAULT_MEM) };
|
|
assert!(!pool.is_null());
|
|
let buffer = unsafe { ZSTDMT_rust_buffer_pool_get(pool) };
|
|
assert!(!buffer.start.is_null());
|
|
let sample = b"multithreaded-buffer-pool";
|
|
unsafe {
|
|
ptr::copy_nonoverlapping(sample.as_ptr(), buffer.start.cast(), sample.len());
|
|
ZSTDMT_rust_buffer_pool_set_size(pool, buffer.capacity + 1);
|
|
}
|
|
let resized = unsafe { ZSTDMT_rust_buffer_pool_resize(pool, buffer) };
|
|
assert_eq!(resized.capacity, buffer.capacity + 1);
|
|
let contents =
|
|
unsafe { std::slice::from_raw_parts(resized.start.cast::<u8>(), sample.len()) };
|
|
assert_eq!(contents, sample);
|
|
unsafe {
|
|
ZSTDMT_rust_buffer_pool_release(pool, resized);
|
|
ZSTDMT_rust_buffer_pool_free(pool);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn job_table_count_matches_c_power_of_two_contract() {
|
|
assert_eq!(rounded_job_count(0), None);
|
|
assert_eq!(rounded_job_count(1), Some(2));
|
|
assert_eq!(rounded_job_count(3), Some(4));
|
|
assert_eq!(rounded_job_count(4), Some(8));
|
|
assert_eq!(rounded_job_count(255), Some(256));
|
|
assert_eq!(rounded_job_count(256), Some(512));
|
|
}
|
|
|
|
#[test]
|
|
fn job_table_creation_cleans_up_after_sync_init_failure() {
|
|
let _lock = JOB_TABLE_TEST_LOCK.lock().unwrap();
|
|
JOB_TABLE_INIT_CALLS.store(0, Ordering::Relaxed);
|
|
JOB_TABLE_DESTROY_CALLS.store(0, Ordering::Relaxed);
|
|
JOB_TABLE_FAIL_INIT.store(true, Ordering::Relaxed);
|
|
|
|
let mut requested_jobs = 1;
|
|
let job_table = unsafe {
|
|
ZSTDMT_rust_createJobsTable(
|
|
&mut requested_jobs,
|
|
1,
|
|
DEFAULT_MEM,
|
|
probe_job_table_init,
|
|
probe_job_table_destroy,
|
|
)
|
|
};
|
|
|
|
assert!(job_table.is_null());
|
|
assert_eq!(requested_jobs, 2);
|
|
assert_eq!(JOB_TABLE_INIT_CALLS.load(Ordering::Relaxed), 1);
|
|
assert_eq!(JOB_TABLE_DESTROY_CALLS.load(Ordering::Relaxed), 1);
|
|
|
|
JOB_TABLE_FAIL_INIT.store(false, Ordering::Relaxed);
|
|
}
|
|
|
|
#[test]
|
|
fn job_table_expansion_preserves_mask_and_sync_lifecycle() {
|
|
let _lock = JOB_TABLE_TEST_LOCK.lock().unwrap();
|
|
JOB_TABLE_INIT_CALLS.store(0, Ordering::Relaxed);
|
|
JOB_TABLE_DESTROY_CALLS.store(0, Ordering::Relaxed);
|
|
JOB_TABLE_FAIL_INIT.store(false, Ordering::Relaxed);
|
|
|
|
let mut requested_jobs = 1;
|
|
let initial_table =
|
|
unsafe { ZSTDMT_rust_job_table_create(&mut requested_jobs, 1, DEFAULT_MEM) };
|
|
assert!(!initial_table.is_null());
|
|
assert_eq!(requested_jobs, 2);
|
|
|
|
let mut job_table = initial_table;
|
|
let mut job_id_mask = 1;
|
|
assert_eq!(
|
|
unsafe {
|
|
ZSTDMT_rust_expandJobsTable(
|
|
&mut job_table,
|
|
&mut job_id_mask,
|
|
2,
|
|
1,
|
|
DEFAULT_MEM,
|
|
probe_job_table_init,
|
|
probe_job_table_destroy,
|
|
)
|
|
},
|
|
0
|
|
);
|
|
assert!(!job_table.is_null());
|
|
assert_eq!(job_id_mask, 7);
|
|
assert_eq!(JOB_TABLE_INIT_CALLS.load(Ordering::Relaxed), 1);
|
|
assert_eq!(JOB_TABLE_DESTROY_CALLS.load(Ordering::Relaxed), 1);
|
|
|
|
let expanded_table = job_table;
|
|
assert_eq!(
|
|
unsafe {
|
|
ZSTDMT_rust_expandJobsTable(
|
|
&mut job_table,
|
|
&mut job_id_mask,
|
|
2,
|
|
1,
|
|
DEFAULT_MEM,
|
|
probe_job_table_init,
|
|
probe_job_table_destroy,
|
|
)
|
|
},
|
|
0
|
|
);
|
|
assert_eq!(job_table, expanded_table);
|
|
assert_eq!(job_id_mask, 7);
|
|
assert_eq!(JOB_TABLE_INIT_CALLS.load(Ordering::Relaxed), 1);
|
|
assert_eq!(JOB_TABLE_DESTROY_CALLS.load(Ordering::Relaxed), 1);
|
|
|
|
unsafe { ZSTDMT_rust_job_table_free(job_table, 8, 1, DEFAULT_MEM) };
|
|
}
|
|
|
|
#[test]
|
|
fn job_table_expansion_cleans_up_after_sync_init_failure() {
|
|
let _lock = JOB_TABLE_TEST_LOCK.lock().unwrap();
|
|
JOB_TABLE_INIT_CALLS.store(0, Ordering::Relaxed);
|
|
JOB_TABLE_DESTROY_CALLS.store(0, Ordering::Relaxed);
|
|
JOB_TABLE_FAIL_INIT.store(true, Ordering::Relaxed);
|
|
|
|
let mut requested_jobs = 1;
|
|
let initial_table =
|
|
unsafe { ZSTDMT_rust_job_table_create(&mut requested_jobs, 1, DEFAULT_MEM) };
|
|
assert!(!initial_table.is_null());
|
|
|
|
let mut job_table = initial_table;
|
|
let mut job_id_mask = 1;
|
|
assert_eq!(
|
|
unsafe {
|
|
ZSTDMT_rust_expandJobsTable(
|
|
&mut job_table,
|
|
&mut job_id_mask,
|
|
2,
|
|
1,
|
|
DEFAULT_MEM,
|
|
probe_job_table_init,
|
|
probe_job_table_destroy,
|
|
)
|
|
},
|
|
ERROR(ZstdErrorCode::MemoryAllocation)
|
|
);
|
|
assert!(job_table.is_null());
|
|
assert_eq!(job_id_mask, 0);
|
|
assert_eq!(JOB_TABLE_INIT_CALLS.load(Ordering::Relaxed), 1);
|
|
assert_eq!(JOB_TABLE_DESTROY_CALLS.load(Ordering::Relaxed), 2);
|
|
|
|
JOB_TABLE_FAIL_INIT.store(false, Ordering::Relaxed);
|
|
}
|
|
|
|
#[test]
|
|
fn target_job_log_preserves_ldm_and_non_ldm_policy() {
|
|
assert_eq!(
|
|
compute_target_job_log(18, 25, ZSTD_FAST, ZSTD_PS_DISABLE),
|
|
20
|
|
);
|
|
assert_eq!(
|
|
compute_target_job_log(30, 25, ZSTD_FAST, ZSTD_PS_DISABLE),
|
|
ZSTDMT_JOBLOG_MAX
|
|
);
|
|
assert_eq!(
|
|
compute_target_job_log(10, 25, ZSTD_FAST, ZSTD_PS_ENABLE),
|
|
28
|
|
);
|
|
assert_eq!(
|
|
compute_target_job_log(30, 25, ZSTD_BTULTRA2, ZSTD_PS_ENABLE),
|
|
27
|
|
);
|
|
assert_eq!(
|
|
compute_target_job_log(10, 30, ZSTD_BTULTRA2, ZSTD_PS_ENABLE),
|
|
ZSTDMT_JOBLOG_MAX
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_parameters_disable_worker_owned_features() {
|
|
assert_eq!(
|
|
prepare_compression_job_parameters(0, 1, ZSTD_PS_ENABLE, 4),
|
|
ZSTDMT_compressionJobParameters {
|
|
checksumFlag: 1,
|
|
ldmEnable: ZSTD_PS_DISABLE,
|
|
nbWorkers: 0,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compression_job_parameters_clear_checksum_after_first_job() {
|
|
assert_eq!(
|
|
prepare_compression_job_parameters(3, 1, ZSTD_PS_ENABLE, 4),
|
|
ZSTDMT_compressionJobParameters {
|
|
checksumFlag: 0,
|
|
ldmEnable: ZSTD_PS_DISABLE,
|
|
nbWorkers: 0,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
prepare_compression_job_parameters(9, 0, ZSTD_PS_DISABLE, 0),
|
|
ZSTDMT_compressionJobParameters {
|
|
checksumFlag: 0,
|
|
ldmEnable: ZSTD_PS_DISABLE,
|
|
nbWorkers: 0,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn overlap_log_defaults_follow_strategy_groups() {
|
|
assert_eq!(overlap_log_default(ZSTD_FAST), 6);
|
|
assert_eq!(overlap_log_default(ZSTD_DFAST), 6);
|
|
assert_eq!(overlap_log_default(ZSTD_GREEDY), 6);
|
|
assert_eq!(overlap_log_default(ZSTD_LAZY), 6);
|
|
assert_eq!(overlap_log_default(ZSTD_LAZY2), 7);
|
|
assert_eq!(overlap_log_default(ZSTD_BTLAZY2), 7);
|
|
assert_eq!(overlap_log_default(ZSTD_BTOPT), 8);
|
|
assert_eq!(overlap_log_default(ZSTD_BTULTRA), 8);
|
|
assert_eq!(overlap_log_default(ZSTD_BTULTRA2), 9);
|
|
assert_eq!(overlap_log_default(0), 6);
|
|
|
|
assert_eq!(overlap_log(0, ZSTD_BTULTRA2), 9);
|
|
assert_eq!(overlap_log(0, ZSTD_FAST), 6);
|
|
assert_eq!(overlap_log(5, ZSTD_BTULTRA2), 5);
|
|
assert_eq!(overlap_log(9, ZSTD_FAST), 9);
|
|
}
|
|
|
|
#[test]
|
|
fn overlap_size_uses_window_or_target_job_log_as_expected() {
|
|
assert_eq!(
|
|
compute_overlap_size(20, 25, ZSTD_FAST, 0, ZSTD_PS_DISABLE),
|
|
1usize << 17
|
|
);
|
|
assert_eq!(
|
|
compute_overlap_size(20, 25, ZSTD_BTULTRA2, 0, ZSTD_PS_DISABLE),
|
|
1usize << 20
|
|
);
|
|
assert_eq!(
|
|
compute_overlap_size(20, 25, ZSTD_FAST, 1, ZSTD_PS_DISABLE),
|
|
0
|
|
);
|
|
assert_eq!(
|
|
compute_overlap_size(30, 25, ZSTD_FAST, 0, ZSTD_PS_ENABLE),
|
|
1usize << 23
|
|
);
|
|
assert_eq!(
|
|
compute_overlap_size(30, 25, ZSTD_BTULTRA2, 0, ZSTD_PS_ENABLE),
|
|
1usize << 25
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cctx_pool_size_addition_handles_zero_components() {
|
|
assert_eq!(sizeof_cctx_pool(0, 0), 0);
|
|
assert_eq!(sizeof_cctx_pool(37, 0), 37);
|
|
assert_eq!(sizeof_cctx_pool(0, 53), 53);
|
|
}
|
|
|
|
#[test]
|
|
fn cctx_pool_size_addition_preserves_ordinary_sum() {
|
|
assert_eq!(sizeof_cctx_pool(128, 4096), 4224);
|
|
}
|
|
|
|
#[test]
|
|
fn cctx_pool_size_addition_wraps_like_c_size_t() {
|
|
assert_eq!(sizeof_cctx_pool(usize::MAX, 1), 0);
|
|
assert_eq!(sizeof_cctx_pool(usize::MAX - 7, 11), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn buffer_pool_size_addition_handles_zero_components() {
|
|
assert_eq!(sizeof_buffer_pool(0, 0), 0);
|
|
assert_eq!(sizeof_buffer_pool(37, 0), 37);
|
|
assert_eq!(sizeof_buffer_pool(0, 53), 53);
|
|
}
|
|
|
|
#[test]
|
|
fn buffer_pool_size_addition_preserves_ordinary_sum() {
|
|
assert_eq!(sizeof_buffer_pool(128, 4096), 4224);
|
|
}
|
|
|
|
#[test]
|
|
fn buffer_pool_size_addition_wraps_like_c_size_t() {
|
|
assert_eq!(sizeof_buffer_pool(usize::MAX, 1), 0);
|
|
assert_eq!(sizeof_buffer_pool(usize::MAX - 7, 11), 3);
|
|
}
|
|
}
|