feat(compress): move dictionary dispatch into Rust

Dictionary insertion still kept its high-level policy in
zstd_compress.c: short and missing dictionary handling, content-type
selection, magic and dictionary-ID processing, entropy-header loading, and
compressed-block-state reset were all interleaved with the private
match-state content loader.

Move that dispatch into ZSTD_rust_compressInsertDictionary. Rust now owns the
mode decisions, reset ordering, dictionary magic and ID semantics, entropy
loading, and error propagation. C retains only a narrow opaque callback for
ZSTD_loadDictionaryContent because that operation depends on private
ZSTD_MatchState_t, ldmState_t, workspace, and parameter layouts. The focused
Rust tests cover short/full errors, raw and auto callback selection, full
dictionary entropy loading, and noDictIDFlag behavior.

Test Plan:
- `cargo test --manifest-path rust/Cargo.toml dictionary -- --test-threads=1`
  -- 21 passed under the 40 GiB virtual-memory cap.
- `cargo test --manifest-path rust/Cargo.toml --all-targets -- --test-threads=1`
  -- 513 passed under the cap before the test-only clippy sentinel cleanup;
  the focused dictionary suite passed again on the exact staged contents.
- `cargo clippy` lib/benches/tests for `rust` and `rust/cli`, with
  `-D warnings`, and nightly formatting -- passed.
- Native library, CLI, full zstd, fuzzer, zstream, and decode-corpus targets
  -- passed serially under the cap.

GPG signing was attempted but unavailable because no pinentry process was
available; this repository's preceding commits are unsigned, so this commit
uses the explicit unsigned fallback.
This commit is contained in:
2026-07-19 08:49:27 +02:00
parent 423854541b
commit a874ab17fd
2 changed files with 378 additions and 61 deletions
+35 -60
View File
@@ -688,6 +688,19 @@ U32 ZSTD_rust_resolveRepcodeToRawOffset(const U32 rep[ZSTD_REP_NUM],
U32 offBase, U32 ll0);
size_t ZSTD_rust_loadCEntropy(ZSTD_compressedBlockState_t* bs, void* workspace,
const void* dict, size_t dictSize);
/* Rust owns dictionary-ingestion policy. The content loader remains a narrow
* callback because it needs configuration-sensitive C-private layouts. */
typedef size_t (*ZSTD_rust_loadDictionaryContent_f)(
void* matchState, void* ldmState, void* workspaceState,
const void* params, const void* src, size_t srcSize,
int dtlm, int tfp);
size_t ZSTD_rust_compressInsertDictionary(
ZSTD_compressedBlockState_t* bs,
void* matchState, void* ldmState, void* workspaceState,
const void* params, const void* dict, size_t dictSize,
int dictContentType, int dtlm, int tfp,
void* workspace, int noDictIDFlag,
ZSTD_rust_loadDictionaryContent_f loadDictionaryContent);
size_t ZSTD_rust_transferSequencesWBlockDelim(
SeqStore_t* seqStore, ZSTD_SequencePosition* seqPos,
const ZSTD_Sequence* inSeqs, size_t inSeqsSize,
@@ -3566,43 +3579,21 @@ size_t ZSTD_loadCEntropy(ZSTD_compressedBlockState_t* bs, void* workspace,
return ZSTD_rust_loadCEntropy(bs, workspace, dict, dictSize);
}
/* Dictionary format :
* See :
* https://github.com/facebook/zstd/blob/release/doc/zstd_compression_format.md#dictionary-format
*/
/*! ZSTD_loadZstdDictionary() :
* @return : dictID, or an error code
* assumptions : magic number supposed already checked
* dictSize supposed >= 8
*/
static size_t ZSTD_loadZstdDictionary(ZSTD_compressedBlockState_t* bs,
ZSTD_MatchState_t* ms,
ZSTD_cwksp* ws,
ZSTD_CCtx_params const* params,
const void* dict, size_t dictSize,
ZSTD_dictTableLoadMethod_e dtlm,
ZSTD_tableFillPurpose_e tfp,
void* workspace)
/* Keep the match-state/content operation private to C. Rust passes only
* opaque pointers here after it has selected the dictionary path. */
static size_t ZSTD_loadDictionaryContent_callback(
void* matchState, void* ldmState, void* workspaceState,
const void* params, const void* src, size_t srcSize,
int dtlm, int tfp)
{
const BYTE* dictPtr = (const BYTE*)dict;
const BYTE* const dictEnd = dictPtr + dictSize;
size_t dictID;
size_t eSize;
ZSTD_STATIC_ASSERT(HUF_WORKSPACE_SIZE >= (1<<MAX(MLFSELog,LLFSELog)));
assert(dictSize >= 8);
assert(MEM_readLE32(dictPtr) == ZSTD_MAGIC_DICTIONARY);
dictID = params->fParams.noDictIDFlag ? 0 : MEM_readLE32(dictPtr + 4 /* skip magic number */ );
eSize = ZSTD_loadCEntropy(bs, workspace, dict, dictSize);
FORWARD_IF_ERROR(eSize, "ZSTD_loadCEntropy failed");
dictPtr += eSize;
{
size_t const dictContentSize = (size_t)(dictEnd - dictPtr);
FORWARD_IF_ERROR(ZSTD_loadDictionaryContent(
ms, NULL, ws, params, dictPtr, dictContentSize, dtlm, tfp), "");
}
return dictID;
return ZSTD_loadDictionaryContent(
(ZSTD_MatchState_t*)matchState,
(ldmState_t*)ldmState,
(ZSTD_cwksp*)workspaceState,
(const ZSTD_CCtx_params*)params,
src, srcSize,
(ZSTD_dictTableLoadMethod_e)dtlm,
(ZSTD_tableFillPurpose_e)tfp);
}
/** ZSTD_compress_insertDictionary() :
@@ -3619,31 +3610,15 @@ ZSTD_compress_insertDictionary(ZSTD_compressedBlockState_t* bs,
ZSTD_tableFillPurpose_e tfp,
void* workspace)
{
int const noDictIDFlag = (params != NULL && dict != NULL && dictSize >= 8)
? params->fParams.noDictIDFlag
: 0;
DEBUGLOG(4, "ZSTD_compress_insertDictionary (dictSize=%u)", (U32)dictSize);
if ((dict==NULL) || (dictSize<8)) {
RETURN_ERROR_IF(dictContentType == ZSTD_dct_fullDict, dictionary_wrong, "");
return 0;
}
ZSTD_reset_compressedBlockState(bs);
/* dict restricted modes */
if (dictContentType == ZSTD_dct_rawContent)
return ZSTD_loadDictionaryContent(ms, ls, ws, params, dict, dictSize, dtlm, tfp);
if (MEM_readLE32(dict) != ZSTD_MAGIC_DICTIONARY) {
if (dictContentType == ZSTD_dct_auto) {
DEBUGLOG(4, "raw content dictionary detected");
return ZSTD_loadDictionaryContent(
ms, ls, ws, params, dict, dictSize, dtlm, tfp);
}
RETURN_ERROR_IF(dictContentType == ZSTD_dct_fullDict, dictionary_wrong, "");
assert(0); /* impossible */
}
/* dict as full zstd dictionary */
return ZSTD_loadZstdDictionary(
bs, ms, ws, params, dict, dictSize, dtlm, tfp, workspace);
return ZSTD_rust_compressInsertDictionary(
bs, ms, ls, ws, params, dict, dictSize,
(int)dictContentType, (int)dtlm, (int)tfp,
workspace, noDictIDFlag,
ZSTD_loadDictionaryContent_callback);
}
#define ZSTD_USE_CDICT_PARAMS_SRCSIZE_CUTOFF (128 KB)
+343 -1
View File
@@ -15,9 +15,12 @@ use crate::entropy_common::FSE_readNCount;
use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
use crate::fse_compress::FSE_buildCTable_wksp;
use crate::huf_compress::HUF_readCTable;
use crate::zstd_compress_stats::ZSTD_compressedBlockState_t;
use crate::zstd_compress_stats::{
ZSTD_compressedBlockState_t, ZSTD_rust_resetCompressedBlockState,
};
use std::ffi::c_void;
use std::os::raw::{c_int, c_short, c_uint};
use std::ptr;
use std::slice;
const HUF_REPEAT_CHECK: c_int = 1;
@@ -27,6 +30,25 @@ const FSE_REPEAT_VALID: c_int = 2;
const HUF_WORKSPACE_SIZE: usize = (8 << 10) + 512;
const DICTIONARY_ID_AND_MAGIC_SIZE: usize = 8;
const REPCODE_SECTION_SIZE: usize = 12;
const ZSTD_MAGIC_DICTIONARY: u32 = 0xEC30_A437;
const ZSTD_DCT_AUTO: c_int = 0;
const ZSTD_DCT_RAW_CONTENT: c_int = 1;
const ZSTD_DCT_FULL_DICT: c_int = 2;
/// C keeps match-state/content insertion private because it depends on the
/// configuration-sensitive `ZSTD_MatchState_t`, `ldmState_t`, workspace, and
/// parameter layouts. Rust owns the dictionary dispatch and calls this narrow
/// operation only after selecting the raw or full-dictionary path.
pub type LoadDictionaryContentFn = unsafe extern "C" fn(
match_state: *mut c_void,
ldm_state: *mut c_void,
workspace_state: *mut c_void,
params: *const c_void,
src: *const c_void,
src_size: usize,
dtlm: c_int,
tfp: c_int,
) -> usize;
#[inline]
fn dictionary_corrupted() -> usize {
@@ -257,6 +279,107 @@ pub unsafe extern "C" fn ZSTD_rust_loadCEntropy(
offset
}
/// Rust-owned dispatch for C's `ZSTD_compress_insertDictionary()`.
///
/// The callback is the only operation that still crosses back into C. It
/// receives opaque pointers because the content loader needs private C
/// layouts, while Rust owns short-dictionary handling, mode selection, block
/// state reset, dictionary magic/ID handling, and entropy setup.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_compressInsertDictionary(
bs: *mut ZSTD_compressedBlockState_t,
match_state: *mut c_void,
ldm_state: *mut c_void,
workspace_state: *mut c_void,
params: *const c_void,
dict: *const c_void,
dict_size: usize,
dict_content_type: c_int,
dtlm: c_int,
tfp: c_int,
workspace: *mut c_void,
no_dict_id_flag: c_int,
load_dictionary_content: LoadDictionaryContentFn,
) -> usize {
if dict.is_null() || dict_size < DICTIONARY_ID_AND_MAGIC_SIZE {
if dict_content_type == ZSTD_DCT_FULL_DICT {
return ERROR(ZstdErrorCode::DictionaryWrong);
}
return 0;
}
unsafe { ZSTD_rust_resetCompressedBlockState(bs) };
if dict_content_type == ZSTD_DCT_RAW_CONTENT {
return unsafe {
load_dictionary_content(
match_state,
ldm_state,
workspace_state,
params,
dict,
dict_size,
dtlm,
tfp,
)
};
}
let dict_magic = unsafe { u32::from_le(ptr::read_unaligned(dict.cast::<u32>())) };
if dict_magic != ZSTD_MAGIC_DICTIONARY {
if dict_content_type == ZSTD_DCT_AUTO {
return unsafe {
load_dictionary_content(
match_state,
ldm_state,
workspace_state,
params,
dict,
dict_size,
dtlm,
tfp,
)
};
}
if dict_content_type == ZSTD_DCT_FULL_DICT {
return ERROR(ZstdErrorCode::DictionaryWrong);
}
debug_assert!(false, "invalid dictionary content type");
}
let dict_id = if no_dict_id_flag != 0 {
0
} else {
unsafe {
u32::from_le(ptr::read_unaligned(dict.cast::<u8>().add(4).cast::<u32>())) as usize
}
};
let entropy_size = unsafe { ZSTD_rust_loadCEntropy(bs, workspace, dict, dict_size) };
if ERR_isError(entropy_size) {
return entropy_size;
}
if entropy_size > dict_size {
return dictionary_corrupted();
}
let content_result = unsafe {
load_dictionary_content(
match_state,
ptr::null_mut(),
workspace_state,
params,
dict.cast::<u8>().add(entropy_size).cast(),
dict_size - entropy_size,
dtlm,
tfp,
)
};
if ERR_isError(content_result) {
return content_result;
}
dict_id
}
#[cfg(test)]
mod tests {
use super::*;
@@ -277,6 +400,77 @@ mod tests {
Offset,
}
struct DictionaryLoadProbe {
calls: usize,
ldm_state: *mut c_void,
dict: *const c_void,
dict_size: usize,
dtlm: c_int,
tfp: c_int,
}
impl Default for DictionaryLoadProbe {
fn default() -> Self {
Self {
calls: 0,
ldm_state: ptr::null_mut(),
dict: ptr::null(),
dict_size: 0,
dtlm: 0,
tfp: 0,
}
}
}
unsafe extern "C" fn record_dictionary_load(
match_state: *mut c_void,
ldm_state: *mut c_void,
_workspace_state: *mut c_void,
_params: *const c_void,
dict: *const c_void,
dict_size: usize,
dtlm: c_int,
tfp: c_int,
) -> usize {
let probe = unsafe { &mut *match_state.cast::<DictionaryLoadProbe>() };
probe.calls += 1;
probe.ldm_state = ldm_state;
probe.dict = dict;
probe.dict_size = dict_size;
probe.dtlm = dtlm;
probe.tfp = tfp;
37
}
unsafe fn dispatch_for_test(
state: &mut ZSTD_compressedBlockState_t,
probe: &mut DictionaryLoadProbe,
dict: *const c_void,
dict_size: usize,
dict_content_type: c_int,
workspace: *mut c_void,
no_dict_id_flag: c_int,
ldm_state: *mut c_void,
) -> usize {
unsafe {
ZSTD_rust_compressInsertDictionary(
state,
(probe as *mut DictionaryLoadProbe).cast(),
ldm_state,
ptr::null_mut(),
ptr::null(),
dict,
dict_size,
dict_content_type,
1,
2,
workspace,
no_dict_id_flag,
record_dictionary_load,
)
}
}
fn assert_dictionary_corrupted(result: usize) {
assert!(ERR_isError(result));
assert_eq!(
@@ -467,6 +661,154 @@ mod tests {
assert_dictionary_corrupted(result);
}
#[test]
fn short_or_missing_dictionary_preserves_state_and_full_mode_error() {
let mut state =
unsafe { MaybeUninit::<ZSTD_compressedBlockState_t>::zeroed().assume_init() };
state.rep = [9, 10, 11];
let mut probe = DictionaryLoadProbe::default();
let result = unsafe {
dispatch_for_test(
&mut state,
&mut probe,
ptr::null(),
0,
ZSTD_DCT_AUTO,
ptr::null_mut(),
0,
ptr::null_mut(),
)
};
assert_eq!(result, 0);
assert_eq!(state.rep, [9, 10, 11]);
let short_dictionary = [0u8; DICTIONARY_ID_AND_MAGIC_SIZE - 1];
let result = unsafe {
dispatch_for_test(
&mut state,
&mut probe,
short_dictionary.as_ptr().cast(),
short_dictionary.len(),
ZSTD_DCT_FULL_DICT,
ptr::null_mut(),
0,
ptr::null_mut(),
)
};
assert!(ERR_isError(result));
assert_eq!(
ERR_getErrorCode(result),
ZstdErrorCode::DictionaryWrong as i32
);
assert_eq!(state.rep, [9, 10, 11]);
state.rep = [12, 13, 14];
let wrong_magic_dictionary = [0u8; DICTIONARY_ID_AND_MAGIC_SIZE];
let result = unsafe {
dispatch_for_test(
&mut state,
&mut probe,
wrong_magic_dictionary.as_ptr().cast(),
wrong_magic_dictionary.len(),
ZSTD_DCT_FULL_DICT,
ptr::null_mut(),
0,
ptr::null_mut(),
)
};
assert!(ERR_isError(result));
assert_eq!(
ERR_getErrorCode(result),
ZstdErrorCode::DictionaryWrong as i32
);
assert_eq!(state.rep, [1, 4, 8]);
assert_eq!(probe.calls, 0);
}
#[test]
fn raw_and_auto_dictionary_modes_use_the_content_callback() {
let mut state =
unsafe { MaybeUninit::<ZSTD_compressedBlockState_t>::zeroed().assume_init() };
let mut probe = DictionaryLoadProbe::default();
let raw_dictionary = [0xEC, 0x30, 0xA4, 0x37, 0, 0, 0, 0];
let result = unsafe {
dispatch_for_test(
&mut state,
&mut probe,
raw_dictionary.as_ptr().cast(),
raw_dictionary.len(),
ZSTD_DCT_RAW_CONTENT,
ptr::null_mut(),
0,
ptr::dangling_mut::<c_void>(),
)
};
assert_eq!(result, 37);
assert_eq!(probe.calls, 1);
assert_eq!(probe.dict, raw_dictionary.as_ptr().cast());
assert_eq!(probe.dict_size, raw_dictionary.len());
assert_eq!(probe.ldm_state, ptr::dangling_mut::<c_void>());
let auto_dictionary = [0u8; DICTIONARY_ID_AND_MAGIC_SIZE];
let result = unsafe {
dispatch_for_test(
&mut state,
&mut probe,
auto_dictionary.as_ptr().cast(),
auto_dictionary.len(),
ZSTD_DCT_AUTO,
ptr::null_mut(),
0,
ptr::dangling_mut::<c_void>(),
)
};
assert_eq!(result, 37);
assert_eq!(probe.calls, 2);
assert_eq!(probe.dict, auto_dictionary.as_ptr().cast());
assert_eq!(probe.dict_size, auto_dictionary.len());
}
#[test]
fn full_dictionary_loads_entropy_and_returns_or_suppresses_id() {
let dictionary = make_dictionary(ZeroWeight::None);
let mut workspace = [0u32; HUF_WORKSPACE_SIZE / size_of::<u32>()];
let mut state =
unsafe { MaybeUninit::<ZSTD_compressedBlockState_t>::zeroed().assume_init() };
let mut probe = DictionaryLoadProbe::default();
let result = unsafe {
dispatch_for_test(
&mut state,
&mut probe,
dictionary.as_ptr().cast(),
dictionary.len(),
ZSTD_DCT_FULL_DICT,
workspace.as_mut_ptr().cast(),
0,
ptr::dangling_mut::<c_void>(),
)
};
assert_eq!(result, 1234);
assert_eq!(probe.calls, 1);
assert_eq!(probe.ldm_state, ptr::null_mut());
assert_eq!(probe.dict_size, DICT_CONTENT_SIZE);
let result = unsafe {
dispatch_for_test(
&mut state,
&mut probe,
dictionary.as_ptr().cast(),
dictionary.len(),
ZSTD_DCT_FULL_DICT,
workspace.as_mut_ptr().cast(),
1,
ptr::dangling_mut::<c_void>(),
)
};
assert_eq!(result, 0);
assert_eq!(probe.calls, 2);
}
#[test]
fn dict_n_count_repeat_requires_coverage_and_nonzero_weights() {
let mut normalized = [1i16; 4];