Files
zstd-rs/rust/src/dict_builder_zdict.rs
T
ddidderr c52d29369c feat(dict-builder): own entropy analysis in Rust
The zdict entropy pass previously allocated and drove an opaque C compression
context, keeping eight private context symbols in the Rust link surface. Replace
that path with a private Rust-owned analyzer that selects and adjusts parameters
through existing Rust policy leaves, builds dictionary match tables through the
existing Rust matcher leaves, and projects only the field-level lazy and optimal
state those leaves consume. The sequence store and superblock leaf remain the
source of entropy statistics, including the existing sample-size cap and
compressible-block filtering. Backing allocations stay owned by the analyzer,
so no public API or C compression-context layout is introduced.

Test Plan:
- `cargo check --manifest-path rust/Cargo.toml --no-default-features --features compression,dict-builder` -- passed.
- `cargo test --manifest-path rust/Cargo.toml --all-targets --no-default-features --features compression,dict-builder` -- 370 passed.
- Focused entropy-context test -- passed.
- Relevant-feature clippy for the library, benches, and tests with `-D warnings` -- passed.
- `cargo +nightly fmt --manifest-path rust/Cargo.toml -- --check` -- passed.
- `make -C tests -B -j2 test-zstream` -- passed, including both fuzz phases.
2026-07-18 15:58:59 +02:00

2045 lines
68 KiB
Rust

#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::too_many_arguments)]
//! Public dictionary-builder wrappers.
//!
//! This is the Rust translation of `lib/dictBuilder/zdict.c`. The COVER and
//! fastCOVER implementations remain separate translation units, but their
//! finalization calls, the legacy trainer, entropy-header construction, and
//! public helper functions all live here. The entropy statistics pass owns a
//! private Rust context and passes only field-level projections to the Rust
//! matcher leaves.
use crate::bits::ZSTD_highbit32;
use crate::common::{LL_FSE_LOG, MAX_LL, MAX_ML, ML_FSE_LOG, OFF_FSE_LOG, ZSTD_REP_NUM};
use crate::dict_builder_fastcover::{
ZDICT_fastCover_params_t, ZDICT_optimizeTrainFromBuffer_fastCover,
};
use crate::divsufsort::divsufsort;
use crate::errors::{ERR_getErrorName, ERR_isError, ZstdErrorCode, ERROR};
use crate::fse_compress::{FSE_normalizeCount, FSE_writeNCount};
use crate::huf_compress::{HUF_buildCTable_wksp, HUF_writeCTable_wksp};
use crate::mem::{MEM_readLE32, MEM_writeLE32};
use crate::xxhash::XXH64;
use crate::zstd_compress_dictionary::ZSTD_rust_loadCEntropy;
use crate::zstd_compress_params::{
ZSTD_compressionParameters, ZSTD_parameters, ZSTD_rust_params_adjustCParams,
ZSTD_rust_params_allocateChainTable, ZSTD_rust_params_getCParamsFromCCtxParams,
ZSTD_rust_params_makeParams, ZSTD_rust_params_maxNbSeq,
ZSTD_rust_params_resolveRowMatchFinderMode, ZSTD_rust_params_selectCParams,
ZSTD_RUST_CPM_CREATE_CDICT, ZSTD_RUST_CPM_UNKNOWN, ZSTD_RUST_PS_AUTO, ZSTD_RUST_PS_ENABLE,
};
use crate::zstd_compress_sequences::SeqDef;
use crate::zstd_compress_stats::{
SeqStore_t, ZSTD_compressedBlockState_t, ZSTD_rust_resetCompressedBlockState, ZSTD_seqToCodes,
};
use crate::zstd_compress_superblock::ZSTD_rust_compressSuperBlock;
use std::ffi::{c_char, c_void};
use std::mem::{size_of, MaybeUninit};
use std::os::raw::{c_int, c_uint};
use std::ptr;
const ZSTD_MAGIC_DICTIONARY: u32 = 0xEC30_A437;
const ZSTD_CLEVEL_DEFAULT: c_int = 3;
const ZSTD_BLOCKSIZE_MAX: usize = 1 << 17;
const HUF_WORKSPACE_SIZE: usize = (8 << 10) + 512;
const HUF_CTABLE_WORKSPACE_SIZE_U32: usize = 4 * (255 + 1) + 192;
const ZDICT_DICTSIZE_MIN: usize = 256;
const ZDICT_CONTENTSIZE_MIN: usize = 128;
const ZDICT_MAX_SAMPLES_SIZE: usize = 2000 << 20;
const ZDICT_MIN_SAMPLES_SIZE: usize = ZDICT_CONTENTSIZE_MIN * 4;
const DICTLISTSIZE_DEFAULT: usize = 10_000;
const NOISELENGTH: usize = 32;
const MINRATIO: usize = 4;
const LLIMIT: usize = 64;
const MINMATCHLENGTH: usize = 7;
const MAXREPOFFSET: usize = 1024;
const OFFCODE_MAX: usize = 30;
/// ABI-compatible `ZDICT_params_t` from `zdict.h`.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZDICT_params_t {
pub compressionLevel: c_int,
pub notificationLevel: c_uint,
pub dictID: c_uint,
}
/// ABI-compatible `ZDICT_legacy_params_t` from `zdict.h`.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZDICT_legacy_params_t {
pub selectivityLevel: c_uint,
pub zParams: ZDICT_params_t,
}
#[inline]
fn dictionary_error(code: ZstdErrorCode) -> usize {
ERROR(code)
}
#[inline]
fn read_u16(data: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([data[offset], data[offset + 1]])
}
#[inline]
fn count_common(data: &[u8], input: usize, matched: usize) -> usize {
let start = input;
let mut input = input;
let mut matched = matched;
while input < data.len() && matched < data.len() && data[input] == data[matched] {
input += 1;
matched += 1;
}
input - start
}
#[inline]
fn suffix_at(suffix0: &[i32], c_index: isize) -> usize {
suffix0[(c_index + 1) as usize] as usize
}
#[derive(Clone, Copy, Debug, Default)]
struct DictItem {
pos: u32,
length: u32,
savings: u32,
}
#[inline]
fn init_dict_item(item: &mut DictItem) {
item.pos = 1;
item.length = 0;
item.savings = u32::MAX;
}
fn analyze_pos(
done_marks: &mut [u8],
suffix0: &[i32],
start: usize,
data: &[u8],
min_ratio: usize,
_notification_level: c_uint,
) -> DictItem {
let mut length_list = [0u32; LLIMIT];
let mut cumulative_length = [0u32; LLIMIT];
let mut savings = [0u32; LLIMIT];
let mut pos = suffix_at(suffix0, start as isize);
let mut end = start;
let mut solution = DictItem::default();
done_marks[pos] = 1;
if read_u16(data, pos) == read_u16(data, pos + 2)
|| read_u16(data, pos + 1) == read_u16(data, pos + 3)
|| read_u16(data, pos + 2) == read_u16(data, pos + 4)
{
let pattern = read_u16(data, pos + 4);
let mut pattern_end = 6;
while read_u16(data, pos + pattern_end) == pattern {
pattern_end += 2;
}
if data[pos + pattern_end] == data[pos + pattern_end - 1] {
pattern_end += 1;
}
for offset in 1..pattern_end {
done_marks[pos + offset] = 1;
}
return solution;
}
loop {
end += 1;
let length = count_common(data, pos, suffix_at(suffix0, end as isize));
if length < MINMATCHLENGTH {
break;
}
}
let mut start = start;
loop {
let length = count_common(data, pos, suffix_at(suffix0, start as isize - 1));
if length < MINMATCHLENGTH {
break;
}
start -= 1;
}
if end - start < min_ratio {
for index in start..end {
done_marks[suffix_at(suffix0, index as isize)] = 1;
}
return solution;
}
let mut refined_start = start;
let mut refined_end = end;
let mut mml = MINMATCHLENGTH;
loop {
let mut current_char = 0u8;
let mut current_count = 0usize;
let mut current_id = refined_start;
let mut selected_count = 0usize;
let mut selected_id = current_id;
for id in refined_start..refined_end {
let byte = data[suffix_at(suffix0, id as isize) + mml];
if byte != current_char {
if current_count > selected_count {
selected_count = current_count;
selected_id = current_id;
}
current_id = id;
current_char = byte;
current_count = 0;
}
current_count += 1;
}
if current_count > selected_count {
selected_count = current_count;
selected_id = current_id;
}
if selected_count < min_ratio {
break;
}
refined_start = selected_id;
refined_end = refined_start + selected_count;
mml += 1;
}
start = refined_start;
pos = suffix_at(suffix0, refined_start as isize);
end = start;
loop {
end += 1;
let original_length = count_common(data, pos, suffix_at(suffix0, end as isize));
let length = original_length.min(LLIMIT - 1);
length_list[length] += 1;
if original_length < MINMATCHLENGTH {
break;
}
}
let mut length = MINMATCHLENGTH;
while length >= MINMATCHLENGTH && start > 0 {
let original_length = count_common(data, pos, suffix_at(suffix0, start as isize - 1));
length = original_length.min(LLIMIT - 1);
length_list[length] += 1;
if original_length >= MINMATCHLENGTH {
start -= 1;
}
}
cumulative_length[LLIMIT - 1] = length_list[LLIMIT - 1];
for index in (0..LLIMIT - 1).rev() {
cumulative_length[index] = cumulative_length[index + 1] + length_list[index];
}
let mut useful_length = MINMATCHLENGTH - 1;
for index in (MINMATCHLENGTH..LLIMIT).rev() {
if cumulative_length[index] >= min_ratio as u32 {
useful_length = index;
break;
}
}
let mut max_length = useful_length;
let repeated = data[pos + max_length - 1];
let mut reduced = max_length as u32;
while data[pos + reduced as usize - 2] == repeated {
reduced -= 1;
}
max_length = reduced as usize;
if max_length < MINMATCHLENGTH {
return solution;
}
savings[5] = 0;
for index in MINMATCHLENGTH..=max_length {
savings[index] =
savings[index - 1].wrapping_add(length_list[index].wrapping_mul((index - 3) as u32));
}
solution.pos = pos as u32;
solution.length = max_length as u32;
solution.savings = savings[max_length];
for index in start..end {
let tested_pos = suffix_at(suffix0, index as isize);
let length = if tested_pos == pos {
solution.length as usize
} else {
count_common(data, pos, tested_pos).min(solution.length as usize)
};
for mark in done_marks.iter_mut().skip(tested_pos).take(length) {
*mark = 1;
}
}
solution
}
#[inline]
fn is_included(data: &[u8], input: usize, container: usize, length: usize) -> bool {
data[input..input + length] == data[container..container + length]
}
fn resort_item(table: &mut [DictItem], mut index: usize) {
let item = table[index];
while index > 1 && table[index - 1].savings < item.savings {
table[index] = table[index - 1];
index -= 1;
}
table[index] = item;
}
fn try_merge(table: &mut [DictItem], elt: DictItem, skip: usize, data: &[u8]) -> usize {
let table_size = table[0].pos as usize;
let elt_end = elt.pos as usize + elt.length as usize;
for index in 1..table_size {
if index == skip {
continue;
}
let item_pos = table[index].pos as usize;
if item_pos > elt.pos as usize && item_pos <= elt_end {
let added = item_pos - elt.pos as usize;
table[index].length = table[index].length.wrapping_add(added as u32);
table[index].pos = elt.pos;
table[index].savings = table[index]
.savings
.wrapping_add(elt.savings.wrapping_mul(added as u32) / elt.length)
.wrapping_add(elt.length / 8);
resort_item(table, index);
return index;
}
}
for index in 1..table_size {
if index == skip {
continue;
}
let item_pos = table[index].pos as usize;
let item_end = item_pos + table[index].length as usize;
if item_end >= elt.pos as usize && item_pos < elt.pos as usize {
let added = elt_end as isize - item_end as isize;
table[index].savings = table[index].savings.wrapping_add(elt.length / 8);
if added > 0 {
table[index].length = table[index].length.wrapping_add(added as u32);
table[index].savings = table[index]
.savings
.wrapping_add(elt.savings.wrapping_mul(added as u32) / elt.length);
}
resort_item(table, index);
return index;
}
let left = item_pos;
let right = elt.pos as usize + 1;
if left + 8 <= data.len()
&& right + 8 <= data.len()
&& data[left..left + 8] == data[right..right + 8]
&& is_included(data, left, right, table[index].length as usize)
{
let added = ((elt.length as isize - table[index].length as isize).max(1)) as usize;
table[index].pos = elt.pos;
table[index].savings = table[index]
.savings
.wrapping_add(elt.savings.wrapping_mul(added as u32) / elt.length);
table[index].length =
(elt.length as usize).min(table[index].length as usize + 1) as u32;
return index;
}
}
0
}
fn remove_dict_item(table: &mut [DictItem], id: usize) {
if id == 0 {
return;
}
let max = table[0].pos as usize;
for index in id..max - 1 {
table[index] = table[index + 1];
}
table[0].pos -= 1;
}
fn insert_dict_item(table: &mut [DictItem], max_size: usize, elt: DictItem, data: &[u8]) {
let merge_id = try_merge(table, elt, 0, data);
if merge_id != 0 {
let mut merge = merge_id;
while merge != 0 {
let next = try_merge(table, table[merge], merge, data);
if next != 0 {
remove_dict_item(table, merge);
}
merge = next;
}
return;
}
let mut next_elt = table[0].pos as usize;
if next_elt >= max_size {
next_elt = max_size - 1;
}
let mut current = next_elt - 1;
while table[current].savings < elt.savings {
table[current + 1] = table[current];
current -= 1;
}
table[current + 1] = elt;
table[0].pos = (next_elt + 1) as u32;
}
fn dict_size(table: &[DictItem]) -> usize {
(1..table[0].pos as usize)
.map(|index| table[index].length as usize)
.sum()
}
fn fill_noise(buffer: &mut [u8]) {
let prime2 = 2_246_822_519u32;
let mut accumulator = 2_654_435_761u32;
for byte in buffer {
accumulator = accumulator.wrapping_mul(prime2);
*byte = (accumulator >> 21) as u8;
}
}
fn total_sample_size(file_sizes: &[usize]) -> usize {
file_sizes
.iter()
.fold(0usize, |total, size| total.wrapping_add(*size))
}
fn train_buffer_legacy(
dict_list: &mut [DictItem],
buffer: &[u8],
file_sizes: &[usize],
min_ratio: usize,
notification_level: c_uint,
) -> usize {
let buffer_size = buffer.len() - NOISELENGTH;
let mut suffix0 = vec![0i32; buffer_size.saturating_add(2)];
let mut reverse_suffix = vec![0u32; buffer_size];
let mut done_marks = vec![0u8; buffer_size.saturating_add(16)];
let mut file_pos = vec![0u32; file_sizes.len()];
let mut effective_buffer_size = buffer_size;
let mut effective_files = file_sizes.len();
while effective_buffer_size > ZDICT_MAX_SAMPLES_SIZE {
if effective_files == 0 {
break;
}
effective_files -= 1;
effective_buffer_size -= file_sizes[effective_files];
}
if effective_buffer_size > ZDICT_MAX_SAMPLES_SIZE {
eprintln!(
"sample set too large : reduced to {} MB ...",
ZDICT_MAX_SAMPLES_SIZE >> 20
);
}
if effective_files > 0 {
for index in 1..effective_files {
file_pos[index] = file_pos[index - 1].wrapping_add(file_sizes[index - 1] as u32);
}
}
let result = unsafe {
divsufsort(
buffer.as_ptr(),
suffix0.as_mut_ptr().add(1),
effective_buffer_size as c_int,
0,
)
};
if result != 0 {
return dictionary_error(ZstdErrorCode::Generic);
}
suffix0[0] = effective_buffer_size as i32;
suffix0[effective_buffer_size + 1] = effective_buffer_size as i32;
for position in 0..effective_buffer_size {
reverse_suffix[suffix_at(&suffix0, position as isize)] = position as u32;
}
done_marks.fill(0);
let mut cursor = 0usize;
while cursor < effective_buffer_size {
if done_marks[cursor] != 0 {
cursor += 1;
continue;
}
let solution = analyze_pos(
&mut done_marks,
&suffix0,
reverse_suffix[cursor] as usize,
buffer,
min_ratio.max(MINRATIO),
notification_level,
);
if solution.length == 0 {
cursor += 1;
continue;
}
insert_dict_item(dict_list, dict_list.len(), solution, buffer);
cursor += solution.length as usize;
}
0
}
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_DICT_NO_DICT: c_int = 0;
const ZSTD_DICT_MATCH_STATE: c_int = 2;
const ZSTD_SEARCH_HASH_CHAIN: c_int = 0;
const ZSTD_SEARCH_BINARY_TREE: c_int = 1;
const ZSTD_SEARCH_ROW_HASH: c_int = 2;
const HASH_READ_SIZE: usize = 8;
const HASHLOG3_MAX: u32 = 17;
const ZSTD_OPT_SIZE: usize = (1 << 12) + 3;
const TMP_WORKSPACE_SIZE: usize = 16 << 10;
const DICT_WINDOW_START_INDEX: u32 = 2;
/// This is the same field-level projection used by `zstd_lazy.c`.
///
/// It is deliberately not a projection of `ZSTD_CCtx_s` or
/// `ZSTD_MatchState_t`; the Rust matcher owns this private value and only its
/// already-established leaf ABI crosses into the matcher module.
#[repr(C)]
struct DictLazyStateProjection {
hashTable: *mut u32,
chainTable: *mut u32,
tagTable: *mut u8,
hashCache: *mut u32,
base: *const u8,
dictBase: *const u8,
nextSrc: *const u8,
dictLimit: u32,
lowLimit: u32,
loadedDictEnd: u32,
nextToUpdate: *mut u32,
lazySkipping: *mut c_int,
hashLog: u32,
chainLog: u32,
minMatch: u32,
searchLog: u32,
windowLog: u32,
rowHashLog: u32,
hashSalt: u64,
hashSaltEntropy: *mut u32,
dictMatchState: *const DictLazyStateProjection,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct DictMatchProjection {
off: u32,
len: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct DictOptimalProjection {
price: c_int,
off: u32,
mlen: u32,
litlen: u32,
rep: [u32; ZSTD_REP_NUM],
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct DictRawSeqProjection {
offset: u32,
litLength: u32,
matchLength: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct DictRawSeqStoreProjection {
seq: *const DictRawSeqProjection,
pos: usize,
posInSequence: usize,
size: usize,
capacity: usize,
}
#[repr(C)]
struct DictOptStateProjection {
litFreq: *mut c_uint,
litLengthFreq: *mut c_uint,
matchLengthFreq: *mut c_uint,
offCodeFreq: *mut c_uint,
matchTable: *mut DictMatchProjection,
priceTable: *mut DictOptimalProjection,
litSum: u32,
litLengthSum: u32,
matchLengthSum: u32,
offCodeSum: u32,
litSumBasePrice: u32,
litLengthSumBasePrice: u32,
matchLengthSumBasePrice: u32,
offCodeSumBasePrice: u32,
priceType: c_int,
symbolCosts: *const c_void,
literalCompressionMode: c_int,
}
/// This is the field-level projection used by `zstd_opt.c` and
/// `zstd_opt_tree.c`. The private C match-state itself never crosses here.
#[repr(C)]
struct DictOptStateProjectionView {
hash_table: *mut u32,
chain_table: *mut u32,
base: *const u8,
dict_base: *const u8,
dict_limit: u32,
low_limit: u32,
loaded_dict_end: u32,
next_to_update: *mut u32,
hash_log: u32,
chain_log: u32,
search_log: u32,
window_log: u32,
use_c_predict: c_int,
hash_table3: *mut u32,
hash_log3: u32,
next_src: *const u8,
min_match: u32,
target_length: u32,
opt: *mut DictOptStateProjection,
dict_match_state: *const DictOptStateProjectionView,
ldm_seq_store: *const DictRawSeqStoreProjection,
window_base: *mut *const u8,
window_dict_limit: *mut u32,
window_low_limit: *mut u32,
huf_ctable: *const usize,
huf_repeat_valid: c_int,
fse_litlength_ctable: *const u32,
fse_matchlength_ctable: *const u32,
fse_offcode_ctable: *const u32,
}
fn try_zeroed_vec<T: Clone + Default>(length: usize) -> Result<Vec<T>, ()> {
let mut result = Vec::new();
result.try_reserve_exact(length).map_err(|_| ())?;
result.resize(length, T::default());
Ok(result)
}
fn table_length(log: u32, entries_per_slot: usize) -> Result<usize, ()> {
1usize
.checked_shl(log)
.and_then(|slots| slots.checked_mul(entries_per_slot))
.ok_or(())
}
#[allow(dead_code)]
struct DictEntropyContext {
cparams: ZSTD_compressionParameters,
row_match_finder: bool,
has_dictionary: bool,
dict_storage: Vec<u8>,
dict_hash_table: Vec<u32>,
dict_chain_table: Vec<u32>,
dict_tag_table: Vec<u8>,
active_hash_table: Vec<u32>,
active_chain_table: Vec<u32>,
active_hash_table3: Vec<u32>,
active_tag_table: Vec<u8>,
active_hash_cache: Vec<u32>,
dict_next_to_update: u32,
dict_lazy_skipping: c_int,
sequences: Vec<SeqDef>,
literals: Vec<u8>,
ll_codes: Vec<u8>,
ml_codes: Vec<u8>,
of_codes: Vec<u8>,
seq_store: SeqStore_t,
opt_lit_freq: Vec<c_uint>,
opt_lit_length_freq: Vec<c_uint>,
opt_match_length_freq: Vec<c_uint>,
opt_off_code_freq: Vec<c_uint>,
opt_match_table: Vec<DictMatchProjection>,
opt_price_table: Vec<DictOptimalProjection>,
active_opt_state: DictOptStateProjection,
}
impl DictEntropyContext {
fn new(
cparams: ZSTD_compressionParameters,
dict_buffer: *const u8,
dict_size: usize,
) -> Result<Self, ()> {
let row_match_finder =
ZSTD_rust_params_resolveRowMatchFinderMode(ZSTD_RUST_PS_AUTO, cparams)
== ZSTD_RUST_PS_ENABLE;
let hash_length = table_length(cparams.hashLog, 1)?;
let chain_length = if ZSTD_rust_params_allocateChainTable(
cparams.strategy,
if row_match_finder {
ZSTD_RUST_PS_ENABLE
} else {
2
},
0,
) != 0
{
table_length(cparams.chainLog, 1)?
} else {
0
};
let has_dictionary = dict_size > HASH_READ_SIZE;
let dictionary_length = dict_size
.checked_add(DICT_WINDOW_START_INDEX as usize)
.ok_or(())?;
let mut dict_storage: Vec<u8> = try_zeroed_vec(dictionary_length)?;
if dict_size != 0 {
if dict_buffer.is_null() {
return Err(());
}
unsafe {
ptr::copy_nonoverlapping(
dict_buffer,
dict_storage
.as_mut_ptr()
.add(DICT_WINDOW_START_INDEX as usize),
dict_size,
);
}
}
let dict_hash_table = if has_dictionary {
try_zeroed_vec(hash_length)?
} else {
Vec::new()
};
let dict_chain_table = if has_dictionary && chain_length != 0 {
try_zeroed_vec(chain_length)?
} else {
Vec::new()
};
let dict_tag_table = if has_dictionary && row_match_finder {
try_zeroed_vec(hash_length)?
} else {
Vec::new()
};
let active_hash_table = try_zeroed_vec(hash_length)?;
let active_chain_table = if chain_length != 0 {
try_zeroed_vec(chain_length)?
} else {
Vec::new()
};
let active_hash_table3 = if cparams.strategy >= ZSTD_BTOPT && cparams.minMatch == 3 {
try_zeroed_vec(table_length(HASHLOG3_MAX.min(cparams.windowLog), 1)?)?
} else {
Vec::new()
};
let active_tag_table = if row_match_finder {
try_zeroed_vec(hash_length)?
} else {
Vec::new()
};
let active_hash_cache = if row_match_finder {
try_zeroed_vec(8)?
} else {
Vec::new()
};
let max_nb_seq =
ZSTD_rust_params_maxNbSeq(ZSTD_BLOCKSIZE_MAX, cparams.minMatch, 0).saturating_add(1);
let sequences: Vec<SeqDef> = try_zeroed_vec(max_nb_seq)?;
let literals: Vec<u8> = try_zeroed_vec(ZSTD_BLOCKSIZE_MAX)?;
let ll_codes: Vec<u8> = try_zeroed_vec(max_nb_seq)?;
let ml_codes: Vec<u8> = try_zeroed_vec(max_nb_seq)?;
let of_codes: Vec<u8> = try_zeroed_vec(max_nb_seq)?;
let opt_lit_freq: Vec<c_uint> = try_zeroed_vec(256)?;
let opt_lit_length_freq: Vec<c_uint> = try_zeroed_vec(MAX_LL + 1)?;
let opt_match_length_freq: Vec<c_uint> = try_zeroed_vec(MAX_ML + 1)?;
let opt_off_code_freq: Vec<c_uint> = try_zeroed_vec(crate::common::MAX_OFF + 1)?;
let opt_match_table: Vec<DictMatchProjection> = try_zeroed_vec(ZSTD_OPT_SIZE)?;
let opt_price_table: Vec<DictOptimalProjection> = try_zeroed_vec(ZSTD_OPT_SIZE)?;
let active_opt_state = DictOptStateProjection {
litFreq: opt_lit_freq.as_ptr().cast_mut(),
litLengthFreq: opt_lit_length_freq.as_ptr().cast_mut(),
matchLengthFreq: opt_match_length_freq.as_ptr().cast_mut(),
offCodeFreq: opt_off_code_freq.as_ptr().cast_mut(),
matchTable: opt_match_table.as_ptr().cast_mut(),
priceTable: opt_price_table.as_ptr().cast_mut(),
litSum: 0,
litLengthSum: 0,
matchLengthSum: 0,
offCodeSum: 0,
litSumBasePrice: 0,
litLengthSumBasePrice: 0,
matchLengthSumBasePrice: 0,
offCodeSumBasePrice: 0,
priceType: 0,
symbolCosts: ptr::null(),
literalCompressionMode: 0,
};
let seq_store = SeqStore_t {
sequencesStart: sequences.as_ptr().cast_mut(),
sequences: sequences.as_ptr().cast_mut(),
litStart: literals.as_ptr().cast_mut(),
lit: literals.as_ptr().cast_mut(),
llCode: ll_codes.as_ptr().cast_mut(),
mlCode: ml_codes.as_ptr().cast_mut(),
ofCode: of_codes.as_ptr().cast_mut(),
maxNbSeq: max_nb_seq,
maxNbLit: ZSTD_BLOCKSIZE_MAX,
longLengthType: 0,
longLengthPos: 0,
};
let mut context = Self {
cparams,
row_match_finder,
has_dictionary,
dict_storage,
dict_hash_table,
dict_chain_table,
dict_tag_table,
active_hash_table,
active_chain_table,
active_hash_table3,
active_tag_table,
active_hash_cache,
dict_next_to_update: DICT_WINDOW_START_INDEX,
dict_lazy_skipping: 0,
sequences,
literals,
ll_codes,
ml_codes,
of_codes,
seq_store,
opt_lit_freq,
opt_lit_length_freq,
opt_match_length_freq,
opt_off_code_freq,
opt_match_table,
opt_price_table,
active_opt_state,
};
context.load_dictionary_tables();
Ok(context)
}
fn load_dictionary_tables(&mut self) {
if !self.has_dictionary {
return;
}
let dict_base = self.dict_storage.as_ptr();
let dict_end = unsafe { dict_base.add(self.dict_storage.len()) };
let strategy = self.cparams.strategy;
let min_match = self.cparams.minMatch;
let hash_log = self.cparams.hashLog;
let chain_log = self.cparams.chainLog;
unsafe {
match strategy {
ZSTD_FAST => crate::zstd_fast::ZSTD_rust_fillHashTable(
self.dict_hash_table.as_mut_ptr(),
dict_base,
DICT_WINDOW_START_INDEX,
dict_end.cast(),
hash_log,
min_match,
1,
1,
),
ZSTD_DFAST => crate::zstd_double_fast::ZSTD_rust_fillDoubleHashTable(
self.dict_hash_table.as_mut_ptr(),
self.dict_chain_table.as_mut_ptr(),
dict_base,
DICT_WINDOW_START_INDEX,
dict_end.cast(),
hash_log,
chain_log,
min_match,
1,
1,
),
ZSTD_GREEDY | ZSTD_LAZY | ZSTD_LAZY2 => {
let row_log = self.cparams.searchLog.clamp(4, 6);
let mut state = DictLazyStateProjection {
hashTable: self.dict_hash_table.as_mut_ptr(),
chainTable: self.dict_chain_table.as_mut_ptr(),
tagTable: self.dict_tag_table.as_mut_ptr(),
hashCache: ptr::null_mut(),
base: dict_base,
dictBase: dict_base,
nextSrc: dict_end,
dictLimit: DICT_WINDOW_START_INDEX,
lowLimit: DICT_WINDOW_START_INDEX,
loadedDictEnd: self.dict_storage.len() as u32,
nextToUpdate: ptr::addr_of_mut!(self.dict_next_to_update),
lazySkipping: ptr::null_mut(),
hashLog: hash_log,
chainLog: chain_log,
minMatch: min_match,
searchLog: self.cparams.searchLog,
windowLog: self.cparams.windowLog,
rowHashLog: hash_log - row_log,
hashSalt: 0,
hashSaltEntropy: ptr::null_mut(),
dictMatchState: ptr::null(),
};
if self.row_match_finder {
self.dict_tag_table.fill(0);
crate::zstd_lazy::ZSTD_rust_lazy_row_update(
(&mut state as *mut DictLazyStateProjection).cast(),
dict_end.sub(HASH_READ_SIZE).cast(),
);
} else {
crate::zstd_lazy::ZSTD_rust_lazy_insertAndFindFirstIndex(
(&mut state as *mut DictLazyStateProjection).cast(),
dict_end.sub(HASH_READ_SIZE).cast(),
);
}
}
ZSTD_BTLAZY2 | ZSTD_BTOPT | ZSTD_BTULTRA | ZSTD_BTULTRA2 => {
let mut state = Self::make_opt_projection(
self.cparams,
self.dict_hash_table.as_mut_ptr(),
self.dict_chain_table.as_mut_ptr(),
ptr::null_mut(),
ptr::addr_of_mut!(self.dict_next_to_update),
dict_base,
dict_base,
dict_end,
DICT_WINDOW_START_INDEX,
DICT_WINDOW_START_INDEX,
self.dict_storage.len() as u32,
ptr::null_mut(),
ptr::null(),
);
crate::zstd_opt::ZSTD_rust_opt_updateTree(
(&mut state as *mut DictOptStateProjectionView).cast(),
dict_end.sub(HASH_READ_SIZE).cast(),
dict_end.cast(),
min_match,
ZSTD_DICT_NO_DICT,
);
}
_ => {}
}
}
self.dict_next_to_update = self.dict_storage.len() as u32;
}
fn reset_active_tables(&mut self) {
self.active_hash_table.fill(0);
self.active_chain_table.fill(0);
self.active_hash_table3.fill(0);
self.active_tag_table.fill(0);
self.active_hash_cache.fill(0);
self.dict_lazy_skipping = 0;
self.active_opt_state.litSum = 0;
self.active_opt_state.litLengthSum = 0;
self.active_opt_state.matchLengthSum = 0;
self.active_opt_state.offCodeSum = 0;
self.active_opt_state.litSumBasePrice = 0;
self.active_opt_state.litLengthSumBasePrice = 0;
self.active_opt_state.matchLengthSumBasePrice = 0;
self.active_opt_state.offCodeSumBasePrice = 0;
self.active_opt_state.priceType = 0;
self.opt_lit_freq.fill(0);
self.opt_lit_length_freq.fill(0);
self.opt_match_length_freq.fill(0);
self.opt_off_code_freq.fill(0);
self.opt_match_table.fill(DictMatchProjection::default());
self.opt_price_table.fill(DictOptimalProjection::default());
}
#[allow(clippy::too_many_arguments)]
fn make_opt_projection(
cparams: ZSTD_compressionParameters,
hash_table: *mut u32,
chain_table: *mut u32,
hash_table3: *mut u32,
next_to_update: *mut u32,
base: *const u8,
dict_base: *const u8,
next_src: *const u8,
dict_limit: u32,
low_limit: u32,
loaded_dict_end: u32,
opt: *mut DictOptStateProjection,
dict_match_state: *const DictOptStateProjectionView,
) -> DictOptStateProjectionView {
DictOptStateProjectionView {
hash_table,
chain_table,
base,
dict_base,
dict_limit,
low_limit,
loaded_dict_end,
next_to_update,
hash_log: cparams.hashLog,
chain_log: cparams.chainLog,
search_log: cparams.searchLog,
window_log: cparams.windowLog,
use_c_predict: 0,
hash_table3,
hash_log3: HASHLOG3_MAX.min(cparams.windowLog),
next_src,
min_match: cparams.minMatch,
target_length: cparams.targetLength,
opt,
dict_match_state,
ldm_seq_store: ptr::null(),
window_base: ptr::null_mut(),
window_dict_limit: ptr::null_mut(),
window_low_limit: ptr::null_mut(),
huf_ctable: ptr::null(),
huf_repeat_valid: 0,
fse_litlength_ctable: ptr::null(),
fse_matchlength_ctable: ptr::null(),
fse_offcode_ctable: ptr::null(),
}
}
#[allow(clippy::too_many_arguments)]
fn compress_sample(
&mut self,
workplace: &mut [u8],
params: &ZSTD_parameters,
src: *const c_void,
mut src_size: usize,
) -> bool {
let block_size_max = ZSTD_BLOCKSIZE_MAX.min(1usize << params.cParams.windowLog);
src_size = src_size.min(block_size_max);
if src_size < HASH_READ_SIZE {
return false;
}
self.reset_active_tables();
self.seq_store.sequences = self.seq_store.sequencesStart;
self.seq_store.lit = self.seq_store.litStart;
self.seq_store.longLengthType = 0;
self.seq_store.longLengthPos = 0;
let source = src.cast::<u8>();
let mut reps = [1u32, 4, 8];
let dict_mode = if self.has_dictionary {
ZSTD_DICT_MATCH_STATE
} else {
ZSTD_DICT_NO_DICT
};
let last_literals = unsafe {
match self.cparams.strategy {
ZSTD_FAST if self.has_dictionary => {
crate::zstd_fast::ZSTD_rust_compressBlock_fast_dictMatchState(
self.active_hash_table.as_mut_ptr(),
source,
0,
self.cparams.hashLog,
self.cparams.minMatch,
self.cparams.targetLength,
(&mut self.seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
src,
src_size,
self.dict_hash_table.as_ptr(),
self.dict_storage.as_ptr(),
DICT_WINDOW_START_INDEX,
self.dict_storage.as_ptr().add(self.dict_storage.len()),
self.cparams.hashLog,
0,
)
}
ZSTD_FAST => crate::zstd_fast::ZSTD_rust_compressBlock_fast(
self.active_hash_table.as_mut_ptr(),
source,
0,
0,
self.cparams.hashLog,
self.cparams.minMatch,
self.cparams.targetLength,
self.cparams.windowLog,
(&mut self.seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
src,
src_size,
),
ZSTD_DFAST if self.has_dictionary => {
crate::zstd_double_fast::ZSTD_rust_compressBlock_doubleFast_dictMatchState(
self.active_hash_table.as_mut_ptr(),
self.active_chain_table.as_mut_ptr(),
source,
0,
self.cparams.hashLog,
self.cparams.chainLog,
self.cparams.minMatch,
(&mut self.seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
src,
src_size,
self.dict_hash_table.as_ptr(),
self.dict_chain_table.as_ptr(),
self.dict_storage.as_ptr(),
DICT_WINDOW_START_INDEX,
self.dict_storage.as_ptr().add(self.dict_storage.len()),
self.cparams.hashLog,
self.cparams.chainLog,
0,
)
}
ZSTD_DFAST => crate::zstd_double_fast::ZSTD_rust_compressBlock_doubleFast(
self.active_hash_table.as_mut_ptr(),
self.active_chain_table.as_mut_ptr(),
source,
0,
0,
self.cparams.hashLog,
self.cparams.chainLog,
self.cparams.minMatch,
self.cparams.windowLog,
(&mut self.seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
src,
src_size,
),
ZSTD_GREEDY | ZSTD_LAZY | ZSTD_LAZY2 | ZSTD_BTLAZY2 => {
let row_log = self.cparams.searchLog.clamp(4, 6);
let mut active_next = 0u32;
let mut active_lazy_skipping = 0;
let mut active_hash_salt_entropy = 0u32;
let dict_state = DictLazyStateProjection {
hashTable: self.dict_hash_table.as_mut_ptr(),
chainTable: self.dict_chain_table.as_mut_ptr(),
tagTable: self.dict_tag_table.as_mut_ptr(),
hashCache: ptr::null_mut(),
base: self.dict_storage.as_ptr(),
dictBase: self.dict_storage.as_ptr(),
nextSrc: self.dict_storage.as_ptr().add(self.dict_storage.len()),
dictLimit: DICT_WINDOW_START_INDEX,
lowLimit: DICT_WINDOW_START_INDEX,
loadedDictEnd: self.dict_storage.len() as u32,
nextToUpdate: ptr::addr_of_mut!(self.dict_next_to_update),
lazySkipping: ptr::null_mut(),
hashLog: self.cparams.hashLog,
chainLog: self.cparams.chainLog,
minMatch: self.cparams.minMatch,
searchLog: self.cparams.searchLog,
windowLog: self.cparams.windowLog,
rowHashLog: self.cparams.hashLog - row_log,
hashSalt: 0,
hashSaltEntropy: ptr::null_mut(),
dictMatchState: ptr::null(),
};
let dict_state = if self.has_dictionary {
ptr::addr_of!(dict_state)
} else {
ptr::null()
};
let mut active_state = DictLazyStateProjection {
hashTable: self.active_hash_table.as_mut_ptr(),
chainTable: self.active_chain_table.as_mut_ptr(),
tagTable: self.active_tag_table.as_mut_ptr(),
hashCache: self.active_hash_cache.as_mut_ptr(),
base: source,
dictBase: source,
nextSrc: source.add(src_size),
dictLimit: 0,
lowLimit: 0,
loadedDictEnd: 0,
nextToUpdate: ptr::addr_of_mut!(active_next),
lazySkipping: ptr::addr_of_mut!(active_lazy_skipping),
hashLog: self.cparams.hashLog,
chainLog: self.cparams.chainLog,
minMatch: self.cparams.minMatch,
searchLog: self.cparams.searchLog,
windowLog: self.cparams.windowLog,
rowHashLog: self.cparams.hashLog - row_log,
hashSalt: 0,
hashSaltEntropy: ptr::addr_of_mut!(active_hash_salt_entropy),
dictMatchState: dict_state,
};
let search_method = if self.row_match_finder {
ZSTD_SEARCH_ROW_HASH
} else if self.cparams.strategy == ZSTD_BTLAZY2 {
ZSTD_SEARCH_BINARY_TREE
} else {
ZSTD_SEARCH_HASH_CHAIN
};
let depth = match self.cparams.strategy {
ZSTD_GREEDY => 0,
ZSTD_LAZY => 1,
ZSTD_LAZY2 | ZSTD_BTLAZY2 => 2,
_ => 0,
};
crate::zstd_lazy::ZSTD_rust_compressBlock_lazy(
(&mut active_state as *mut DictLazyStateProjection).cast(),
(&mut self.seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
src,
src_size,
search_method,
depth,
dict_mode,
)
}
ZSTD_BTOPT | ZSTD_BTULTRA | ZSTD_BTULTRA2 => {
let mut window_base = source;
let mut window_dict_limit = 0u32;
let mut window_low_limit = 0u32;
let mut active_next = 0u32;
let dict_state = Self::make_opt_projection(
self.cparams,
self.dict_hash_table.as_mut_ptr(),
self.dict_chain_table.as_mut_ptr(),
ptr::null_mut(),
ptr::addr_of_mut!(self.dict_next_to_update),
self.dict_storage.as_ptr(),
self.dict_storage.as_ptr(),
self.dict_storage.as_ptr().add(self.dict_storage.len()),
DICT_WINDOW_START_INDEX,
DICT_WINDOW_START_INDEX,
self.dict_storage.len() as u32,
ptr::null_mut(),
ptr::null(),
);
let dict_state = if self.has_dictionary {
ptr::addr_of!(dict_state)
} else {
ptr::null()
};
let mut active_state = Self::make_opt_projection(
self.cparams,
self.active_hash_table.as_mut_ptr(),
self.active_chain_table.as_mut_ptr(),
self.active_hash_table3.as_mut_ptr(),
ptr::addr_of_mut!(active_next),
source,
source,
source.add(src_size),
0,
0,
0,
ptr::addr_of_mut!(self.active_opt_state),
dict_state,
);
active_state.window_base = ptr::addr_of_mut!(window_base);
active_state.window_dict_limit = ptr::addr_of_mut!(window_dict_limit);
active_state.window_low_limit = ptr::addr_of_mut!(window_low_limit);
crate::zstd_opt::ZSTD_rust_compressBlock_opt(
(&mut active_state as *mut DictOptStateProjectionView).cast(),
(&mut self.seq_store as *mut SeqStore_t).cast(),
reps.as_mut_ptr(),
src,
src_size,
if self.cparams.strategy == ZSTD_BTOPT {
0
} else {
2
},
dict_mode,
)
}
_ => return false,
}
};
if last_literals > src_size {
return false;
}
if last_literals != 0 {
unsafe {
ptr::copy_nonoverlapping(
source.add(src_size - last_literals),
self.seq_store.lit,
last_literals,
);
self.seq_store.lit = self.seq_store.lit.add(last_literals);
}
}
let mut previous_block =
unsafe { MaybeUninit::<ZSTD_compressedBlockState_t>::zeroed().assume_init() };
let mut next_block =
unsafe { MaybeUninit::<ZSTD_compressedBlockState_t>::zeroed().assume_init() };
unsafe {
ZSTD_rust_resetCompressedBlockState(&mut previous_block);
ZSTD_rust_resetCompressedBlockState(&mut next_block);
}
let mut superblock_workspace = [0u64; TMP_WORKSPACE_SIZE / size_of::<u64>()];
let compressed = unsafe {
ZSTD_rust_compressSuperBlock(
(&self.seq_store as *const SeqStore_t).cast(),
(&previous_block as *const ZSTD_compressedBlockState_t).cast(),
(&mut next_block as *mut ZSTD_compressedBlockState_t).cast(),
self.cparams.strategy,
0,
superblock_workspace.as_mut_ptr().cast(),
TMP_WORKSPACE_SIZE,
0,
self.cparams.windowLog,
0,
workplace.as_mut_ptr().cast(),
workplace.len(),
src,
src_size,
0,
)
};
!ERR_isError(compressed) && compressed != 0
}
}
fn count_entropy_stats(
context: &mut DictEntropyContext,
workplace: &mut [u8],
params: &ZSTD_parameters,
count_lit: &mut [u32; 256],
offset_counts: &mut [u32; OFFCODE_MAX + 1],
match_counts: &mut [u32; MAX_ML + 1],
lit_counts: &mut [u32; MAX_LL + 1],
rep_offsets: &mut [u32; MAXREPOFFSET],
src: *const c_void,
src_size: usize,
) {
if !context.compress_sample(workplace, params, src, src_size) {
return;
}
let store = &mut context.seq_store;
let literal_count = unsafe { store.lit.offset_from(store.litStart) as usize };
for byte in unsafe { std::slice::from_raw_parts(store.litStart, literal_count) } {
count_lit[*byte as usize] += 1;
}
let nb_sequences = unsafe { store.sequences.offset_from(store.sequencesStart) as usize };
unsafe { ZSTD_seqToCodes(store) };
for index in 0..nb_sequences {
let of_code = unsafe { *store.ofCode.add(index) as usize };
let ml_code = unsafe { *store.mlCode.add(index) as usize };
let ll_code = unsafe { *store.llCode.add(index) as usize };
if of_code <= OFFCODE_MAX {
offset_counts[of_code] += 1;
}
if ml_code <= MAX_ML {
match_counts[ml_code] += 1;
}
if ll_code <= MAX_LL {
lit_counts[ll_code] += 1;
}
}
if nb_sequences >= 2 {
let first = unsafe { &*store.sequencesStart.cast::<SeqDef>() };
let second = unsafe { &*store.sequencesStart.add(1).cast::<SeqDef>() };
let offset1 = first.offBase.wrapping_sub(ZSTD_REP_NUM as u32);
let offset2 = second.offBase.wrapping_sub(ZSTD_REP_NUM as u32);
rep_offsets[if offset1 < MAXREPOFFSET as u32 {
offset1 as usize
} else {
0
}] += 3;
rep_offsets[if offset2 < MAXREPOFFSET as u32 {
offset2 as usize
} else {
0
}] += 1;
}
}
unsafe fn analyze_entropy(
dst_buffer: *mut u8,
max_dst_size: usize,
compression_level: c_int,
src_buffer: *const u8,
file_sizes: *const usize,
nb_files: c_uint,
dict_buffer: *const u8,
dict_buffer_size: usize,
_notification_level: c_uint,
) -> usize {
let offcode_max = ZSTD_highbit32((dict_buffer_size + (128 << 10)) as u32) as usize;
if offcode_max > OFFCODE_MAX {
return dictionary_error(ZstdErrorCode::DictionaryCreationFailed);
}
let file_sizes = if nb_files == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(file_sizes, nb_files as usize) }
};
let total_src_size = total_sample_size(file_sizes);
let average_sample_size = total_src_size / (nb_files as usize + usize::from(nb_files == 0));
let mut count_lit = [1u32; 256];
let mut offset_counts = [0u32; OFFCODE_MAX + 1];
offset_counts[..=offcode_max].fill(1);
let mut match_counts = [1u32; MAX_ML + 1];
let mut lit_counts = [1u32; MAX_LL + 1];
let mut rep_offsets = [0u32; MAXREPOFFSET];
rep_offsets[1] = 1;
rep_offsets[4] = 1;
rep_offsets[8] = 1;
let level = if compression_level == 0 {
ZSTD_CLEVEL_DEFAULT
} else {
compression_level
};
let src_size_hint = if average_sample_size == 0 {
u64::MAX
} else {
average_sample_size as u64
};
let selected_params = ZSTD_rust_params_selectCParams(
level,
src_size_hint,
dict_buffer_size,
ZSTD_RUST_CPM_UNKNOWN,
);
let analysis_cparams = ZSTD_rust_params_adjustCParams(
selected_params,
src_size_hint,
dict_buffer_size,
ZSTD_RUST_CPM_UNKNOWN,
ZSTD_RUST_PS_AUTO,
);
let params = ZSTD_rust_params_makeParams(analysis_cparams);
let cdict_cparams = ZSTD_rust_params_getCParamsFromCCtxParams(
0,
0,
u64::MAX,
dict_buffer_size,
ZSTD_RUST_CPM_CREATE_CDICT,
ZSTD_RUST_PS_AUTO,
0,
analysis_cparams,
ZSTD_RUST_PS_AUTO,
0,
);
let mut context = match DictEntropyContext::new(cdict_cparams, dict_buffer, dict_buffer_size) {
Ok(context) => context,
Err(()) => return dictionary_error(ZstdErrorCode::MemoryAllocation),
};
let mut workplace = match try_zeroed_vec(ZSTD_BLOCKSIZE_MAX) {
Ok(workplace) => workplace,
Err(()) => return dictionary_error(ZstdErrorCode::MemoryAllocation),
};
if src_buffer.is_null() && total_src_size != 0 {
return dictionary_error(ZstdErrorCode::MemoryAllocation);
}
let mut source_offset = 0usize;
for &sample_size in file_sizes {
count_entropy_stats(
&mut context,
&mut workplace,
&params,
&mut count_lit,
&mut offset_counts,
&mut match_counts,
&mut lit_counts,
&mut rep_offsets,
unsafe { src_buffer.add(source_offset).cast() },
sample_size,
);
source_offset = source_offset.wrapping_add(sample_size);
}
let mut huf_table = [0usize; 257];
let mut huf_workspace = [0u32; HUF_CTABLE_WORKSPACE_SIZE_U32];
let mut huff_log = 11u32;
let mut written = unsafe {
HUF_buildCTable_wksp(
huf_table.as_mut_ptr(),
count_lit.as_ptr(),
255,
huff_log,
huf_workspace.as_mut_ptr().cast(),
size_of_val(&huf_workspace),
)
};
if ERR_isError(written) {
return written;
}
if written == 8 {
for count in count_lit.iter_mut().skip(1) {
*count = 2;
}
count_lit[0] = 4;
count_lit[253] = 1;
count_lit[254] = 1;
written = unsafe {
HUF_buildCTable_wksp(
huf_table.as_mut_ptr(),
count_lit.as_ptr(),
255,
huff_log,
huf_workspace.as_mut_ptr().cast(),
size_of_val(&huf_workspace),
)
};
if ERR_isError(written) {
return written;
}
}
huff_log = written as u32;
let mut offcode_ncount = [0i16; OFFCODE_MAX + 1];
let mut match_ncount = [0i16; MAX_ML + 1];
let mut lit_ncount = [0i16; MAX_LL + 1];
let total = offset_counts[..=offcode_max]
.iter()
.fold(0usize, |sum, count| sum + *count as usize);
let mut off_log = OFF_FSE_LOG as u32;
let mut match_log = ML_FSE_LOG as u32;
let mut lit_log = LL_FSE_LOG as u32;
let normalized = unsafe {
FSE_normalizeCount(
offcode_ncount.as_mut_ptr(),
off_log,
offset_counts.as_ptr(),
total,
offcode_max as u32,
1,
)
};
if ERR_isError(normalized) {
return normalized;
}
off_log = normalized as u32;
let total = match_counts
.iter()
.fold(0usize, |sum, count| sum + *count as usize);
let normalized = unsafe {
FSE_normalizeCount(
match_ncount.as_mut_ptr(),
match_log,
match_counts.as_ptr(),
total,
MAX_ML as u32,
1,
)
};
if ERR_isError(normalized) {
return normalized;
}
match_log = normalized as u32;
let total = lit_counts
.iter()
.fold(0usize, |sum, count| sum + *count as usize);
let normalized = unsafe {
FSE_normalizeCount(
lit_ncount.as_mut_ptr(),
lit_log,
lit_counts.as_ptr(),
total,
MAX_LL as u32,
1,
)
};
if ERR_isError(normalized) {
return normalized;
}
lit_log = normalized as u32;
let mut dst = dst_buffer;
let mut remaining = max_dst_size;
let mut entropy_size = unsafe {
HUF_writeCTable_wksp(
dst.cast(),
remaining,
huf_table.as_ptr(),
255,
huff_log,
huf_workspace.as_mut_ptr().cast(),
size_of_val(&huf_workspace),
)
};
if ERR_isError(entropy_size) {
return entropy_size;
}
dst = unsafe { dst.add(entropy_size) };
remaining -= entropy_size;
let header_size = unsafe {
FSE_writeNCount(
dst.cast(),
remaining,
offcode_ncount.as_ptr(),
OFFCODE_MAX as u32,
off_log,
)
};
if ERR_isError(header_size) {
return header_size;
}
entropy_size += header_size;
dst = unsafe { dst.add(header_size) };
remaining -= header_size;
let header_size = unsafe {
FSE_writeNCount(
dst.cast(),
remaining,
match_ncount.as_ptr(),
MAX_ML as u32,
match_log,
)
};
if ERR_isError(header_size) {
return header_size;
}
entropy_size += header_size;
dst = unsafe { dst.add(header_size) };
remaining -= header_size;
let header_size = unsafe {
FSE_writeNCount(
dst.cast(),
remaining,
lit_ncount.as_ptr(),
MAX_LL as u32,
lit_log,
)
};
if ERR_isError(header_size) {
return header_size;
}
entropy_size += header_size;
dst = unsafe { dst.add(header_size) };
remaining -= header_size;
if remaining < 12 {
return dictionary_error(ZstdErrorCode::DstSizeTooSmall);
}
unsafe {
MEM_writeLE32(dst.cast(), 1);
MEM_writeLE32(dst.add(4).cast(), 4);
MEM_writeLE32(dst.add(8).cast(), 8);
}
entropy_size + 12
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_isError(error_code: usize) -> c_uint {
c_uint::from(ERR_isError(error_code))
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_getErrorName(error_code: usize) -> *const c_char {
ERR_getErrorName(error_code)
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_getDictID(dict_buffer: *const c_void, dict_size: usize) -> c_uint {
if dict_size < 8 {
return 0;
}
if unsafe { MEM_readLE32(dict_buffer) } != ZSTD_MAGIC_DICTIONARY {
return 0;
}
unsafe { MEM_readLE32(dict_buffer.cast::<u8>().add(4).cast()) }
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_getDictHeaderSize(
dict_buffer: *const c_void,
dict_size: usize,
) -> usize {
if dict_size <= 8 || unsafe { MEM_readLE32(dict_buffer) } != ZSTD_MAGIC_DICTIONARY {
return dictionary_error(ZstdErrorCode::DictionaryCorrupted);
}
let mut state = unsafe { MaybeUninit::<ZSTD_compressedBlockState_t>::zeroed().assume_init() };
let mut workspace = vec![0u32; HUF_WORKSPACE_SIZE / size_of::<u32>()];
unsafe {
ZSTD_rust_resetCompressedBlockState(&mut state);
ZSTD_rust_loadCEntropy(
&mut state,
workspace.as_mut_ptr().cast(),
dict_buffer,
dict_size,
)
}
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_finalizeDictionary(
dict_buffer: *mut c_void,
dict_buffer_capacity: usize,
custom_dict_content: *const c_void,
mut dict_content_size: usize,
samples_buffer: *const c_void,
samples_sizes: *const usize,
nb_samples: c_uint,
params: ZDICT_params_t,
) -> usize {
if dict_buffer_capacity < dict_content_size || dict_buffer_capacity < ZDICT_DICTSIZE_MIN {
return dictionary_error(ZstdErrorCode::DstSizeTooSmall);
}
let mut header = [0u8; 256];
unsafe {
MEM_writeLE32(header.as_mut_ptr().cast(), ZSTD_MAGIC_DICTIONARY);
let hash = XXH64(custom_dict_content, dict_content_size, 0);
let compliant_id = (hash % ((1u64 << 31) - 32_768)) as u32 + 32_768;
let dict_id = if params.dictID != 0 {
params.dictID
} else {
compliant_id
};
MEM_writeLE32(header.as_mut_ptr().add(4).cast(), dict_id);
}
let entropy_size = analyze_entropy(
header.as_mut_ptr().add(8),
header.len() - 8,
if params.compressionLevel == 0 {
ZSTD_CLEVEL_DEFAULT
} else {
params.compressionLevel
},
samples_buffer.cast(),
samples_sizes,
nb_samples,
custom_dict_content.cast(),
dict_content_size,
params.notificationLevel,
);
if ERR_isError(entropy_size) {
return entropy_size;
}
let header_size = 8 + entropy_size;
if header_size + dict_content_size > dict_buffer_capacity {
dict_content_size = dict_buffer_capacity - header_size;
}
let min_content_size = 8usize;
let padding_size = if dict_content_size < min_content_size {
if header_size + min_content_size > dict_buffer_capacity {
return dictionary_error(ZstdErrorCode::DstSizeTooSmall);
}
min_content_size - dict_content_size
} else {
0
};
let dictionary_size = header_size + padding_size + dict_content_size;
unsafe {
let output = dict_buffer.cast::<u8>();
let content = output.add(header_size + padding_size);
ptr::copy(custom_dict_content.cast::<u8>(), content, dict_content_size);
ptr::copy_nonoverlapping(header.as_ptr(), output, header_size);
ptr::write_bytes(output.add(header_size), 0, padding_size);
}
dictionary_size
}
unsafe fn add_entropy_tables_advanced(
dict_buffer: *mut c_void,
dict_content_size: usize,
dict_buffer_capacity: usize,
samples_buffer: *const c_void,
samples_sizes: *const usize,
nb_samples: c_uint,
params: ZDICT_params_t,
) -> usize {
if dict_buffer_capacity < 8 || dict_content_size > dict_buffer_capacity {
return dictionary_error(ZstdErrorCode::DstSizeTooSmall);
}
let content = dict_buffer
.cast::<u8>()
.add(dict_buffer_capacity - dict_content_size);
let entropy_size = analyze_entropy(
dict_buffer.cast::<u8>().add(8),
dict_buffer_capacity - 8,
if params.compressionLevel == 0 {
ZSTD_CLEVEL_DEFAULT
} else {
params.compressionLevel
},
samples_buffer.cast(),
samples_sizes,
nb_samples,
content,
dict_content_size,
params.notificationLevel,
);
if ERR_isError(entropy_size) {
return entropy_size;
}
let header_size = 8 + entropy_size;
unsafe {
MEM_writeLE32(dict_buffer, ZSTD_MAGIC_DICTIONARY);
let hash = XXH64(content.cast(), dict_content_size, 0);
let compliant_id = (hash % ((1u64 << 31) - 32_768)) as u32 + 32_768;
MEM_writeLE32(
dict_buffer.cast::<u8>().add(4).cast(),
if params.dictID != 0 {
params.dictID
} else {
compliant_id
},
);
if header_size + dict_content_size < dict_buffer_capacity {
ptr::copy(
content,
dict_buffer.cast::<u8>().add(header_size),
dict_content_size,
);
}
}
dict_buffer_capacity.min(header_size + dict_content_size)
}
unsafe fn train_from_buffer_unsafe_legacy(
dict_buffer: *mut c_void,
max_dict_size: usize,
samples_buffer: &[u8],
samples_sizes: &[usize],
params: ZDICT_legacy_params_t,
) -> usize {
let dict_list_size = DICTLISTSIZE_DEFAULT
.max(samples_sizes.len())
.max(max_dict_size / 16);
let mut dict_list = Vec::<DictItem>::new();
if dict_list.try_reserve_exact(dict_list_size).is_err() {
return dictionary_error(ZstdErrorCode::MemoryAllocation);
}
dict_list.resize(dict_list_size, DictItem::default());
init_dict_item(&mut dict_list[0]);
let selectivity = if params.selectivityLevel == 0 {
9usize
} else {
params.selectivityLevel as usize
};
let min_rep = if selectivity > 30 {
MINRATIO
} else {
samples_sizes.len() >> selectivity
};
let sample_size = total_sample_size(samples_sizes);
if max_dict_size < ZDICT_DICTSIZE_MIN {
return dictionary_error(ZstdErrorCode::DstSizeTooSmall);
}
if sample_size < ZDICT_MIN_SAMPLES_SIZE {
return dictionary_error(ZstdErrorCode::DictionaryCreationFailed);
}
let _ = train_buffer_legacy(
&mut dict_list,
samples_buffer,
samples_sizes,
min_rep,
params.zParams.notificationLevel,
);
let mut content_size = dict_size(&dict_list);
if content_size < ZDICT_CONTENTSIZE_MIN {
return dictionary_error(ZstdErrorCode::DictionaryCreationFailed);
}
let max = dict_list[0].pos as usize;
let mut current_size = 0usize;
let mut count = 1usize;
while count < max {
current_size += dict_list[count].length as usize;
if current_size > max_dict_size {
current_size -= dict_list[count].length as usize;
break;
}
count += 1;
}
dict_list[0].pos = count as u32;
content_size = current_size;
let output = unsafe { std::slice::from_raw_parts_mut(dict_buffer.cast::<u8>(), max_dict_size) };
let mut write_at = max_dict_size;
for item in dict_list.iter().take(dict_list[0].pos as usize).skip(1) {
let length = item.length as usize;
if length > write_at {
return dictionary_error(ZstdErrorCode::Generic);
}
write_at -= length;
if write_at + length > output.len()
|| item.pos as usize + length > samples_buffer.len().saturating_sub(NOISELENGTH)
{
return dictionary_error(ZstdErrorCode::Generic);
}
output[write_at..write_at + length]
.copy_from_slice(&samples_buffer[item.pos as usize..item.pos as usize + length]);
}
add_entropy_tables_advanced(
dict_buffer,
content_size,
max_dict_size,
samples_buffer.as_ptr().cast(),
samples_sizes.as_ptr(),
samples_sizes.len() as c_uint,
params.zParams,
)
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_trainFromBuffer_legacy(
dict_buffer: *mut c_void,
dict_buffer_capacity: usize,
samples_buffer: *const c_void,
samples_sizes: *const usize,
nb_samples: c_uint,
params: ZDICT_legacy_params_t,
) -> usize {
let sizes = if nb_samples == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(samples_sizes, nb_samples as usize) }
};
let sample_size = total_sample_size(sizes);
if sample_size < ZDICT_MIN_SAMPLES_SIZE {
return 0;
}
let mut guarded = Vec::<u8>::new();
if guarded
.try_reserve_exact(sample_size.saturating_add(NOISELENGTH))
.is_err()
{
return dictionary_error(ZstdErrorCode::MemoryAllocation);
}
guarded.resize(sample_size + NOISELENGTH, 0);
if sample_size != 0 {
unsafe {
ptr::copy_nonoverlapping(
samples_buffer.cast::<u8>(),
guarded.as_mut_ptr(),
sample_size,
);
}
}
fill_noise(&mut guarded[sample_size..]);
unsafe {
train_from_buffer_unsafe_legacy(dict_buffer, dict_buffer_capacity, &guarded, sizes, params)
}
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_trainFromBuffer(
dict_buffer: *mut c_void,
dict_buffer_capacity: usize,
samples_buffer: *const c_void,
samples_sizes: *const usize,
nb_samples: c_uint,
) -> usize {
let mut params = ZDICT_fastCover_params_t {
d: 8,
steps: 4,
zParams: crate::dict_builder_cover::ZDICT_params_t {
compressionLevel: ZSTD_CLEVEL_DEFAULT,
..crate::dict_builder_cover::ZDICT_params_t::default()
},
..ZDICT_fastCover_params_t::default()
};
unsafe {
ZDICT_optimizeTrainFromBuffer_fastCover(
dict_buffer,
dict_buffer_capacity,
samples_buffer,
samples_sizes,
nb_samples,
&mut params,
)
}
}
#[no_mangle]
pub unsafe extern "C" fn ZDICT_addEntropyTablesFromBuffer(
dict_buffer: *mut c_void,
dict_content_size: usize,
dict_buffer_capacity: usize,
samples_buffer: *const c_void,
samples_sizes: *const usize,
nb_samples: c_uint,
) -> usize {
unsafe {
add_entropy_tables_advanced(
dict_buffer,
dict_content_size,
dict_buffer_capacity,
samples_buffer,
samples_sizes,
nb_samples,
ZDICT_params_t::default(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;
#[test]
fn helper_abi_and_error_names_match() {
assert_eq!(unsafe { ZDICT_getDictID(ptr::null(), 0) }, 0);
let error = dictionary_error(ZstdErrorCode::DstSizeTooSmall);
assert_eq!(unsafe { ZDICT_isError(error) }, 1);
let name = unsafe { CStr::from_ptr(ZDICT_getErrorName(error)) };
assert!(name
.to_bytes()
.windows(11)
.any(|part| part == b"Destination"));
}
#[test]
fn dictionary_id_reads_only_valid_headers() {
let mut dict = [0u8; 8];
unsafe {
MEM_writeLE32(dict.as_mut_ptr().cast(), ZSTD_MAGIC_DICTIONARY);
MEM_writeLE32(dict.as_mut_ptr().add(4).cast(), 1234);
}
assert_eq!(
unsafe { ZDICT_getDictID(dict.as_ptr().cast(), dict.len()) },
1234
);
dict[0] ^= 1;
assert_eq!(
unsafe { ZDICT_getDictID(dict.as_ptr().cast(), dict.len()) },
0
);
}
#[test]
fn entropy_context_emits_sequences_for_a_repetitive_sample() {
let dictionary = b"the quick brown fox jumps over the lazy dog. ";
let sample: Vec<u8> = (0..512)
.map(|index| dictionary[index % dictionary.len()])
.collect();
let selected = ZSTD_rust_params_selectCParams(
ZSTD_CLEVEL_DEFAULT,
sample.len() as u64,
dictionary.len(),
ZSTD_RUST_CPM_UNKNOWN,
);
let cparams = ZSTD_rust_params_adjustCParams(
selected,
sample.len() as u64,
dictionary.len(),
ZSTD_RUST_CPM_UNKNOWN,
ZSTD_RUST_PS_AUTO,
);
let params = ZSTD_rust_params_makeParams(cparams);
let mut context = DictEntropyContext::new(cparams, dictionary.as_ptr(), dictionary.len())
.expect("the small dictionary context should allocate");
let mut workplace = vec![0u8; ZSTD_BLOCKSIZE_MAX];
assert!(context.compress_sample(
&mut workplace,
&params,
sample.as_ptr().cast(),
sample.len(),
));
let sequence_count = unsafe {
context
.seq_store
.sequences
.offset_from(context.seq_store.sequencesStart)
};
assert!(sequence_count > 0);
}
}