refactor(compress): move dictionary content policy to Rust

Move ZSTD_loadDictionaryContent orchestration into a Rust-owned policy leaf while keeping C-private match-state, LDM, workspace, window, overflow, and matchfinder operations behind narrow callbacks. Preserve both dictionary suffix limits, LDM-before-main-table ordering, dictionary validity publication, strategy-specific loading, and final nextToUpdate publication without changing ZSTD_compress_insertDictionary dispatch semantics.

Add focused policy tests for the two suffix limits and matchfinder path selection. Preserve the existing global overflow helper as a C callback boundary.

Test Plan:
- rustfmt --check rust/src/zstd_compress_dictionary.rs
- git diff --check and git diff --cached --check
- Static diff/rg inspection of the Rust/C ABI, callback order, suffix truncation, and private-state ownership
- No cargo, make, native, fuzz, or heavy tests run by request
This commit is contained in:
2026-07-20 02:37:02 +02:00
parent e7568ae8bd
commit 0ea4a1b6c3
2 changed files with 669 additions and 130 deletions
+359 -2
View File
@@ -6,8 +6,8 @@
//!
//! This is the Rust leaf for `ZSTD_dictNCountRepeat()` and
//! `ZSTD_loadCEntropy()` in `lib/compress/zstd_compress.c`. C retains the
//! original symbol through a thin compatibility shim and keeps dictionary
//! content loading and compression-context ownership on its side.
//! original symbols through thin compatibility shims and keeps the
//! configuration-sensitive dictionary-content operations on its side.
use crate::bits::ZSTD_highbit32;
use crate::common::{LL_FSE_LOG, MAX_LL, MAX_ML, MAX_OFF, ML_FSE_LOG, OFF_FSE_LOG};
@@ -61,6 +61,328 @@ pub type LoadDictionaryContentFn = unsafe extern "C" fn(
tfp: c_int,
) -> usize;
type LoadDictionaryContentAssertFn = unsafe extern "C" fn(context: *mut c_void);
type LoadDictionaryContentAssertWindowEmptyFn =
unsafe extern "C" fn(context: *mut c_void, ldm: c_int);
type LoadDictionaryContentWindowUpdateFn =
unsafe extern "C" fn(context: *mut c_void, ldm: c_int, src: *const c_void, src_size: usize);
type LoadDictionaryContentSetLdmLoadedDictEndFn =
unsafe extern "C" fn(context: *mut c_void, iend: *const c_void, force_window: c_int);
type LoadDictionaryContentPublishMatchStateFn = unsafe extern "C" fn(
context: *mut c_void,
ip: *const c_void,
iend: *const c_void,
force_window: c_int,
deterministic_ref_prefix: c_int,
);
type LoadDictionaryContentFillLdmFn =
unsafe extern "C" fn(context: *mut c_void, ip: *const c_void, iend: *const c_void);
type LoadDictionaryContentOverflowCorrectFn =
unsafe extern "C" fn(context: *mut c_void, ip: *const c_void, iend: *const c_void);
type LoadDictionaryContentFillTableFn =
unsafe extern "C" fn(context: *mut c_void, iend: *const c_void, dtlm: c_int, tfp: c_int);
type LoadDictionaryContentLoadMatchFn =
unsafe extern "C" fn(context: *mut c_void, ip: *const c_void);
type LoadDictionaryContentLoadTreeFn =
unsafe extern "C" fn(context: *mut c_void, ip: *const c_void, iend: *const c_void);
type LoadDictionaryContentPublishFinalIndexFn =
unsafe extern "C" fn(context: *mut c_void, iend: *const c_void);
/// Rust-owned policy projection for `ZSTD_loadDictionaryContent()`.
///
/// All configuration-dependent C layouts stay behind callbacks. The scalar
/// fields are copied by the C adapter so Rust owns suffix selection, window
/// and LDM ordering, index publication, and strategy dispatch without seeing
/// `ZSTD_MatchState_t`, `ldmState_t`, or workspace internals.
#[repr(C)]
pub struct ZSTD_rust_loadDictionaryContentState {
callback_context: *mut c_void,
current_max: usize,
window_start_index: usize,
short_cache_tag_bits: usize,
chunk_size_max: usize,
hash_read_size: usize,
hash_log: usize,
chain_log: usize,
cdict_indices_tagged: usize,
ldm_enabled: usize,
has_ldm_state: usize,
force_window: usize,
deterministic_ref_prefix: usize,
strategy: usize,
use_row_match_finder: usize,
dedicated_dict_search: usize,
dtlm: usize,
tfp: usize,
assert_c_params: LoadDictionaryContentAssertFn,
assert_window_empty: LoadDictionaryContentAssertWindowEmptyFn,
window_update: LoadDictionaryContentWindowUpdateFn,
set_ldm_loaded_dict_end: LoadDictionaryContentSetLdmLoadedDictEndFn,
publish_match_state: LoadDictionaryContentPublishMatchStateFn,
fill_ldm: LoadDictionaryContentFillLdmFn,
overflow_correct: LoadDictionaryContentOverflowCorrectFn,
fill_hash_table: LoadDictionaryContentFillTableFn,
fill_double_hash_table: LoadDictionaryContentFillTableFn,
load_dedicated: LoadDictionaryContentLoadMatchFn,
load_row: LoadDictionaryContentLoadMatchFn,
load_chain: LoadDictionaryContentLoadMatchFn,
load_tree: LoadDictionaryContentLoadTreeFn,
publish_final_index: LoadDictionaryContentPublishFinalIndexFn,
assert_lazy_configuration: LoadDictionaryContentAssertFn,
assert_invalid_strategy: LoadDictionaryContentAssertFn,
}
const _: () = {
assert!(size_of::<LoadDictionaryContentAssertFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentAssertWindowEmptyFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentWindowUpdateFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentSetLdmLoadedDictEndFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentPublishMatchStateFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentFillLdmFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentOverflowCorrectFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentFillTableFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentLoadMatchFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentLoadTreeFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentPublishFinalIndexFn>() == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_loadDictionaryContentState, callback_context) == 0);
assert!(offset_of!(ZSTD_rust_loadDictionaryContentState, current_max) == size_of::<usize>());
assert!(
offset_of!(ZSTD_rust_loadDictionaryContentState, assert_c_params)
== 18 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_loadDictionaryContentState, publish_final_index)
== 31 * size_of::<usize>()
);
assert!(
offset_of!(
ZSTD_rust_loadDictionaryContentState,
assert_lazy_configuration
) == 32 * size_of::<usize>()
);
assert!(
offset_of!(
ZSTD_rust_loadDictionaryContentState,
assert_invalid_strategy
) == 33 * size_of::<usize>()
);
assert!(size_of::<ZSTD_rust_loadDictionaryContentState>() == 34 * size_of::<usize>());
};
const ZSTD_TFP_FOR_CDICT: usize = 1;
const ZSTD_PS_ENABLE: usize = 1;
const ZSTD_FAST_STRATEGY: usize = 1;
const ZSTD_DFAST_STRATEGY: usize = 2;
const ZSTD_GREEDY_STRATEGY: usize = 3;
const ZSTD_LAZY_STRATEGY: usize = 4;
const ZSTD_LAZY2_STRATEGY: usize = 5;
const ZSTD_BTLAZY2_STRATEGY: usize = 6;
const ZSTD_BTOPT_STRATEGY: usize = 7;
const ZSTD_BTULTRA_STRATEGY: usize = 8;
const ZSTD_BTULTRA2_STRATEGY: usize = 9;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DictionaryTablePolicy {
Fast,
DoubleFast,
Dedicated,
Row,
Chain,
Tree,
Invalid,
}
#[inline]
fn dictionary_suffix(src_size: usize, max_size: usize) -> (usize, usize) {
if src_size > max_size {
(src_size - max_size, max_size)
} else {
(0, src_size)
}
}
#[inline]
fn dictionary_initial_max_size(
current_max: usize,
window_start_index: usize,
short_cache_tag_bits: usize,
cdict_indices_tagged: usize,
tfp: usize,
load_ldm_dict: bool,
) -> usize {
let mut max_size = current_max - window_start_index;
if cdict_indices_tagged != 0 && tfp == ZSTD_TFP_FOR_CDICT {
let short_cache_max_size =
(1usize << (32 - short_cache_tag_bits) as u32) - window_start_index;
max_size = max_size.min(short_cache_max_size);
debug_assert!(!load_ldm_dict);
}
max_size
}
#[inline]
fn dictionary_table_max_size(hash_log: usize, chain_log: usize) -> usize {
let max_shift = hash_log
.saturating_add(3)
.max(chain_log.saturating_add(1))
.min(31);
1usize.checked_shl(max_shift as u32).unwrap()
}
#[inline]
fn dictionary_table_policy(
strategy: usize,
dedicated_dict_search: usize,
use_row_match_finder: usize,
) -> DictionaryTablePolicy {
match strategy {
ZSTD_FAST_STRATEGY => DictionaryTablePolicy::Fast,
ZSTD_DFAST_STRATEGY => DictionaryTablePolicy::DoubleFast,
ZSTD_GREEDY_STRATEGY | ZSTD_LAZY_STRATEGY | ZSTD_LAZY2_STRATEGY => {
if dedicated_dict_search != 0 {
DictionaryTablePolicy::Dedicated
} else if use_row_match_finder == ZSTD_PS_ENABLE {
DictionaryTablePolicy::Row
} else {
DictionaryTablePolicy::Chain
}
}
ZSTD_BTLAZY2_STRATEGY
| ZSTD_BTOPT_STRATEGY
| ZSTD_BTULTRA_STRATEGY
| ZSTD_BTULTRA2_STRATEGY => DictionaryTablePolicy::Tree,
_ => DictionaryTablePolicy::Invalid,
}
}
unsafe fn load_dictionary_content(
state: &ZSTD_rust_loadDictionaryContentState,
src: *const c_void,
src_size: usize,
) -> usize {
unsafe { (state.assert_c_params)(state.callback_context) };
let load_ldm_dict = state.ldm_enabled != 0 && state.has_ldm_state != 0;
let src_bytes = src.cast::<u8>();
let iend = unsafe { src_bytes.add(src_size) };
let (first_offset, mut loaded_src_size) = dictionary_suffix(
src_size,
dictionary_initial_max_size(
state.current_max,
state.window_start_index,
state.short_cache_tag_bits,
state.cdict_indices_tagged,
state.tfp,
load_ldm_dict,
),
);
let mut ip = unsafe { src_bytes.add(first_offset) };
if loaded_src_size > state.chunk_size_max {
unsafe { (state.assert_window_empty)(state.callback_context, 0) };
if load_ldm_dict {
unsafe { (state.assert_window_empty)(state.callback_context, 1) };
}
}
unsafe { (state.window_update)(state.callback_context, 0, ip.cast(), loaded_src_size) };
if load_ldm_dict {
unsafe {
(state.window_update)(state.callback_context, 1, ip.cast(), loaded_src_size);
(state.set_ldm_loaded_dict_end)(
state.callback_context,
iend.cast(),
(state.force_window != 0) as c_int,
);
(state.fill_ldm)(state.callback_context, ip.cast(), iend.cast());
}
}
let (_, table_src_size) = dictionary_suffix(
loaded_src_size,
dictionary_table_max_size(state.hash_log, state.chain_log),
);
if table_src_size != loaded_src_size {
ip = unsafe { iend.sub(table_src_size) };
loaded_src_size = table_src_size;
}
unsafe {
(state.publish_match_state)(
state.callback_context,
ip.cast(),
iend.cast(),
(state.force_window != 0) as c_int,
(state.deterministic_ref_prefix != 0) as c_int,
)
};
if loaded_src_size <= state.hash_read_size {
return 0;
}
unsafe { (state.overflow_correct)(state.callback_context, ip.cast(), iend.cast()) };
let hash_ip = unsafe { iend.sub(state.hash_read_size) };
match dictionary_table_policy(
state.strategy,
state.dedicated_dict_search,
state.use_row_match_finder,
) {
DictionaryTablePolicy::Fast => unsafe {
(state.fill_hash_table)(
state.callback_context,
iend.cast(),
state.dtlm as c_int,
state.tfp as c_int,
)
},
DictionaryTablePolicy::DoubleFast => unsafe {
(state.fill_double_hash_table)(
state.callback_context,
iend.cast(),
state.dtlm as c_int,
state.tfp as c_int,
)
},
DictionaryTablePolicy::Dedicated => unsafe {
(state.load_dedicated)(state.callback_context, hash_ip.cast())
},
DictionaryTablePolicy::Row => unsafe {
(state.assert_lazy_configuration)(state.callback_context);
(state.load_row)(state.callback_context, hash_ip.cast())
},
DictionaryTablePolicy::Chain => unsafe {
(state.assert_lazy_configuration)(state.callback_context);
(state.load_chain)(state.callback_context, hash_ip.cast())
},
DictionaryTablePolicy::Tree => unsafe {
(state.load_tree)(state.callback_context, hash_ip.cast(), iend.cast())
},
DictionaryTablePolicy::Invalid => unsafe {
(state.assert_invalid_strategy)(state.callback_context)
},
}
unsafe { (state.publish_final_index)(state.callback_context, iend.cast()) };
0
}
/// Orchestrate raw/full dictionary content loading while C retains private
/// state mutation and matchfinder operations behind narrow callbacks.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_loadDictionaryContent(
state: *const ZSTD_rust_loadDictionaryContentState,
src: *const c_void,
src_size: usize,
) -> usize {
if state.is_null() || src.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
unsafe { load_dictionary_content(&*state, src, src_size) }
}
/// These callbacks are deliberately opaque: C retains the private CCtx,
/// local/prefix dictionary layouts, allocation, and CDict lifetime rules.
/// Rust owns the stage checks and the order in which clear/assign operations
@@ -5251,4 +5573,39 @@ mod tests {
assert_eq!(dict_n_count_repeat(&normalized, 3, 3), FSE_REPEAT_CHECK);
assert_eq!(dict_n_count_repeat(&normalized, 2, 2), FSE_REPEAT_CHECK);
}
#[test]
fn dictionary_content_policy_preserves_both_suffix_limits() {
assert_eq!(dictionary_suffix(100, 20), (80, 20));
assert_eq!(dictionary_suffix(20, 20), (0, 20));
assert_eq!(
dictionary_initial_max_size(100, 2, 28, 1, ZSTD_TFP_FOR_CDICT, false),
14
);
assert_eq!(dictionary_table_max_size(4, 5), 128);
}
#[test]
fn dictionary_content_policy_selects_matchfinder_path() {
assert_eq!(
dictionary_table_policy(ZSTD_FAST_STRATEGY, 0, 0),
DictionaryTablePolicy::Fast
);
assert_eq!(
dictionary_table_policy(ZSTD_GREEDY_STRATEGY, 1, ZSTD_PS_ENABLE),
DictionaryTablePolicy::Dedicated
);
assert_eq!(
dictionary_table_policy(ZSTD_LAZY_STRATEGY, 0, ZSTD_PS_ENABLE),
DictionaryTablePolicy::Row
);
assert_eq!(
dictionary_table_policy(ZSTD_LAZY2_STRATEGY, 0, 2),
DictionaryTablePolicy::Chain
);
assert_eq!(
dictionary_table_policy(ZSTD_BTULTRA2_STRATEGY, 0, 0),
DictionaryTablePolicy::Tree
);
}
}