zstd_complete/dictionary/
mod.rs1mod cover;
26mod frequency;
27mod reservoir;
28
29use crate::dictionary::reservoir::create_sample;
30use crate::{
31 bit_io::BitWriter,
32 decoding::dictionary::MAGIC_NUM,
33 fse::fse_encoder::{default_ll_table, default_ml_table, default_of_table},
34 huff0::huff0_encoder::HuffmanTable,
35};
36use core::cmp::Reverse;
37use core::fmt;
38use cover::*;
39use std::{
40 boxed::Box,
41 collections::{BinaryHeap, HashMap},
42 fs::{self, File},
43 io::{self, Read},
44 path::{Path, PathBuf},
45 vec::Vec,
46};
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct DictionaryTrainerOptions {
51 dictionary_size: usize,
52 dictionary_id: u32,
53 max_training_bytes: usize,
54}
55
56impl DictionaryTrainerOptions {
57 pub const fn new(dictionary_size: usize, dictionary_id: u32) -> Self {
59 Self {
60 dictionary_size,
61 dictionary_id,
62 max_training_bytes: 64 * 1024 * 1024,
63 }
64 }
65
66 pub const fn with_max_training_bytes(mut self, bytes: usize) -> Self {
68 self.max_training_bytes = bytes;
69 self
70 }
71
72 pub const fn dictionary_size(&self) -> usize {
73 self.dictionary_size
74 }
75
76 pub const fn dictionary_id(&self) -> u32 {
77 self.dictionary_id
78 }
79
80 pub const fn max_training_bytes(&self) -> usize {
81 self.max_training_bytes
82 }
83}
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum DictionaryTrainingError {
88 EmptySamples,
89 ZeroDictionaryId,
90 DictionaryTooSmall,
91 TrainingLimitTooSmall,
92}
93
94impl fmt::Display for DictionaryTrainingError {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 formatter.write_str(match self {
97 Self::EmptySamples => "dictionary training requires at least one non-empty sample",
98 Self::ZeroDictionaryId => "a formatted dictionary ID must be non-zero",
99 Self::DictionaryTooSmall => "dictionary size is too small for formatted metadata",
100 Self::TrainingLimitTooSmall => "training byte limit must be greater than zero",
101 })
102 }
103}
104
105impl std::error::Error for DictionaryTrainingError {}
106
107pub fn train_dictionary<S: AsRef<[u8]>>(
115 samples: &[S],
116 options: DictionaryTrainerOptions,
117) -> Result<Vec<u8>, DictionaryTrainingError> {
118 if options.dictionary_id == 0 {
119 return Err(DictionaryTrainingError::ZeroDictionaryId);
120 }
121 if options.max_training_bytes == 0 {
122 return Err(DictionaryTrainingError::TrainingLimitTooSmall);
123 }
124
125 let mut training = Vec::new();
126 let mut literal_counts = [1_usize; 256];
127 for sample in samples {
128 let sample = sample.as_ref();
129 let remaining = options.max_training_bytes.saturating_sub(training.len());
130 let retained = &sample[..sample.len().min(remaining)];
131 for &byte in retained {
132 literal_counts[usize::from(byte)] = literal_counts[usize::from(byte)].saturating_add(1);
133 }
134 training.extend_from_slice(retained);
135 if training.len() == options.max_training_bytes {
136 break;
137 }
138 }
139 if training.is_empty() {
140 return Err(DictionaryTrainingError::EmptySamples);
141 }
142
143 let huffman = HuffmanTable::build_from_counts(&literal_counts);
144 let mut metadata = Vec::new();
145 metadata.extend_from_slice(&MAGIC_NUM);
146 metadata.extend_from_slice(&options.dictionary_id.to_le_bytes());
147 metadata.extend_from_slice(huffman.table_description());
148 append_fse_description(&mut metadata, default_of_table());
149 append_fse_description(&mut metadata, default_ml_table());
150 append_fse_description(&mut metadata, default_ll_table());
151
152 let minimum_content = 8;
153 let Some(content_budget) = options
154 .dictionary_size
155 .checked_sub(metadata.len().saturating_add(12))
156 .filter(|budget| *budget >= minimum_content)
157 else {
158 return Err(DictionaryTrainingError::DictionaryTooSmall);
159 };
160
161 let mut content = Vec::new();
162 create_raw_dict_from_source(
163 training.as_slice(),
164 training.len(),
165 &mut content,
166 content_budget,
167 );
168 if content.len() < minimum_content {
169 let missing = minimum_content - content.len();
170 content.extend(training.iter().copied().cycle().take(missing));
171 }
172 content.truncate(content_budget);
173
174 metadata.extend_from_slice(&1_u32.to_le_bytes());
175 metadata.extend_from_slice(&(content.len().min(4) as u32).to_le_bytes());
176 metadata.extend_from_slice(&(content.len().min(8) as u32).to_le_bytes());
177 metadata.extend_from_slice(&content);
178 Ok(metadata)
179}
180
181fn append_fse_description(output: &mut Vec<u8>, table: crate::fse::fse_encoder::FSETable) {
182 let mut writer = BitWriter::new();
183 table.write_table(&mut writer).expect("owned dictionary table allocation failed");
184 output.extend_from_slice(&writer.dump().expect("owned dictionary output allocation failed"));
185}
186
187pub(super) struct DictParams {
192 pub segment_size: u32,
202}
203
204pub fn create_raw_dict_from_dir<P: AsRef<Path>, W: io::Write>(
222 path: P,
223 output: &mut W,
224 dict_size: usize,
225) -> Result<(), io::Error> {
226 let mut file_paths: Vec<PathBuf> = Vec::new();
228 let dir: fs::ReadDir = fs::read_dir(path)?;
229 fn recurse_read(dir: fs::ReadDir, file_paths: &mut Vec<PathBuf>) -> Result<(), io::Error> {
230 for entry in dir {
231 let entry = entry?;
232 if entry.file_type()?.is_dir() {
233 recurse_read(fs::read_dir(entry.path())?, file_paths)?;
234 } else {
235 file_paths.push(entry.path());
236 }
237 }
238 Ok(())
239 }
240 recurse_read(dir, &mut file_paths)?;
241
242 let mut total_file_len: u64 = 0;
244 let mut file_handles: Vec<fs::File> = Vec::new();
245 for path in file_paths {
246 let handle = File::open(path)?;
247 total_file_len += handle.metadata()?.len();
248 file_handles.push(handle);
249 }
250 let empty_reader: Box<dyn Read> = Box::new(io::empty());
251 let chained_files = file_handles
252 .iter()
253 .fold(empty_reader, |acc, reader| Box::new(acc.chain(reader)));
254
255 create_raw_dict_from_source(chained_files, total_file_len as usize, output, dict_size);
257 Ok(())
258}
259
260pub fn create_raw_dict_from_source<R: io::Read, W: io::Write>(
274 mut source: R,
275 source_size: usize,
276 output: &mut W,
277 dict_size: usize,
278) {
279 if source_size < 16 {
280 let mut source = source;
281 let mut buf = Vec::new();
282 source
283 .read_to_end(&mut buf)
284 .expect("Could not read from source");
285 output.write_all(&buf).expect("Could not write to output");
286 return;
287 }
288 vprintln!("create_dict: creating {dict_size} byte dict from {source_size} byte source");
289 if dict_size == 0 {
290 return;
291 }
292
293 let mut source_data = Vec::with_capacity(source_size);
294 source
295 .read_to_end(&mut source_data)
296 .expect("can read input");
297 if source_data.is_empty() {
298 return;
299 }
300
301 if source_data.len() < K {
302 output
303 .write_all(&source_data[..usize::min(dict_size, source_data.len())])
304 .expect("can write to output");
305 return;
306 }
307
308 let params = DictParams { segment_size: 2048 };
309 let segment_size = params.segment_size as usize;
310 let num_segments = usize::max(1, source_data.len().div_ceil(segment_size));
311 let sample_divisor = usize::min(usize::max(1, source_data.len() / (2 * num_segments)), 256);
315 let sample_size = usize::max(K, source_data.len() / sample_divisor);
316 vprintln!("create_dict: creating {sample_size} byte sample of collection");
317 let collection_sample = create_sample(&mut source_data.as_slice(), sample_size);
318
319 let mut pool: BinaryHeap<Reverse<Segment>> = BinaryHeap::new();
325 let (_, epoch_kmers) = compute_epoch_info(¶ms, dict_size, source_data.len() / K);
326 let epoch_size = usize::min(source_data.len(), usize::max(K, epoch_kmers * K));
327 let num_epochs = source_data.len().div_ceil(epoch_size);
328 vprintln!("create_dict: computed epoch info, using {num_epochs} epochs of {epoch_size} bytes");
329 let mut ctx = Context {
330 frequencies: HashMap::with_capacity(epoch_size / K),
331 };
332 for (epoch_index, current_epoch) in source_data.chunks(epoch_size).enumerate() {
335 let Some(best_segment) =
336 pick_best_segment(¶ms, &mut ctx, current_epoch, &collection_sample)
337 else {
338 continue;
339 };
340 vprintln!(
341 "\tcreate_dict: epoch {}/{} has best segment score {}",
342 epoch_index + 1,
343 num_epochs,
344 best_segment.score
345 );
346 pool.push(Reverse(best_segment));
347 ctx.frequencies.clear();
349 }
350 vprintln!(
351 "create_dict: {num_epochs} epochs written, writing {} segments",
352 pool.len()
353 );
354 let mut remaining = dict_size;
357 while let Some(segment) = pool.pop() {
358 if remaining == 0 {
359 break;
360 }
361 let bytes_to_write = usize::min(remaining, segment.0.raw.len());
362 output
363 .write_all(&segment.0.raw[..bytes_to_write])
364 .expect("can write to output");
365 remaining -= bytes_to_write;
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::{
372 create_raw_dict_from_source, train_dictionary, DictionaryTrainerOptions,
373 DictionaryTrainingError,
374 };
375 use std::{io::Read, vec::Vec};
376
377 #[test]
378 fn raw_dict_builder_scores_epochs_after_sampling() {
379 let mut source = Vec::new();
380 for _ in 0..512 {
381 source.extend_from_slice(b"aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb");
382 source.extend_from_slice(b"ccccccccccccccccdddddddddddddddd");
383 }
384 let mut dictionary = Vec::new();
385
386 create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
387
388 assert!(!dictionary.is_empty());
389 assert!(dictionary.len() <= 1024);
390 }
391
392 #[test]
393 fn raw_dict_builder_handles_tiny_sources() {
394 let source = b"tiny";
395 let mut dictionary = Vec::new();
396
397 create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
398
399 assert_eq!(dictionary, source);
400 }
401
402 #[test]
403 fn formatted_dictionary_is_usable_by_encoder_and_decoder() {
404 let samples = [
405 b"customer=alice action=login status=success".repeat(100),
406 b"customer=bob action=logout status=success".repeat(100),
407 ];
408 let dictionary =
409 train_dictionary(&samples, DictionaryTrainerOptions::new(2048, 0xC0DE)).unwrap();
410 let decoded = crate::decoding::Dictionary::decode_dict(&dictionary).unwrap();
411 assert_eq!(decoded.id, 0xC0DE);
412 assert!(dictionary.len() <= 2048);
413
414 let input = b"customer=alice action=logout status=success".repeat(200);
415 let encoder_dictionary = crate::encoding::EncoderDictionary::copy(&dictionary).unwrap();
416 let compressed = crate::encoding::encode_all(
417 input.as_slice(),
418 crate::encoding::EncoderOptions::default().with_dictionary(encoder_dictionary),
419 )
420 .unwrap();
421 let mut c_decoder =
422 zstd::stream::read::Decoder::with_dictionary(compressed.as_slice(), &dictionary)
423 .unwrap();
424 let mut output = Vec::new();
425 c_decoder.read_to_end(&mut output).unwrap();
426 assert_eq!(output, input);
427 }
428
429 #[test]
430 fn formatted_dictionary_options_are_validated() {
431 assert_eq!(
432 train_dictionary(&[b"sample"], DictionaryTrainerOptions::new(1024, 0)),
433 Err(DictionaryTrainingError::ZeroDictionaryId)
434 );
435 assert_eq!(
436 train_dictionary(&[b"sample"], DictionaryTrainerOptions::new(8, 1)),
437 Err(DictionaryTrainingError::DictionaryTooSmall)
438 );
439 }
440}
441
442#[test]
443fn create_raw_dict_from_source_no_panics_on_small_input() {
444 use std::io::Cursor;
445
446 for size in 0..1024 {
447 let input = alloc::vec![b'A'; size];
448 let mut output = Vec::new();
449
450 create_raw_dict_from_source(Cursor::new(input.clone()), input.len(), &mut output, 64);
451 }
452}