mod cover;
mod frequency;
mod reservoir;
use crate::dictionary::reservoir::create_sample;
use crate::{
bit_io::BitWriter,
decoding::dictionary::MAGIC_NUM,
fse::fse_encoder::{default_ll_table, default_ml_table, default_of_table},
huff0::huff0_encoder::HuffmanTable,
};
use core::cmp::Reverse;
use core::fmt;
use cover::*;
use std::{
boxed::Box,
collections::{BinaryHeap, HashMap},
fs::{self, File},
io::{self, Read},
path::{Path, PathBuf},
vec::Vec,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DictionaryTrainerOptions {
dictionary_size: usize,
dictionary_id: u32,
max_training_bytes: usize,
}
impl DictionaryTrainerOptions {
pub const fn new(dictionary_size: usize, dictionary_id: u32) -> Self {
Self {
dictionary_size,
dictionary_id,
max_training_bytes: 64 * 1024 * 1024,
}
}
pub const fn with_max_training_bytes(mut self, bytes: usize) -> Self {
self.max_training_bytes = bytes;
self
}
pub const fn dictionary_size(&self) -> usize {
self.dictionary_size
}
pub const fn dictionary_id(&self) -> u32 {
self.dictionary_id
}
pub const fn max_training_bytes(&self) -> usize {
self.max_training_bytes
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DictionaryTrainingError {
EmptySamples,
ZeroDictionaryId,
DictionaryTooSmall,
TrainingLimitTooSmall,
}
impl fmt::Display for DictionaryTrainingError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::EmptySamples => "dictionary training requires at least one non-empty sample",
Self::ZeroDictionaryId => "a formatted dictionary ID must be non-zero",
Self::DictionaryTooSmall => "dictionary size is too small for formatted metadata",
Self::TrainingLimitTooSmall => "training byte limit must be greater than zero",
})
}
}
impl std::error::Error for DictionaryTrainingError {}
pub fn train_dictionary<S: AsRef<[u8]>>(
samples: &[S],
options: DictionaryTrainerOptions,
) -> Result<Vec<u8>, DictionaryTrainingError> {
if options.dictionary_id == 0 {
return Err(DictionaryTrainingError::ZeroDictionaryId);
}
if options.max_training_bytes == 0 {
return Err(DictionaryTrainingError::TrainingLimitTooSmall);
}
let mut training = Vec::new();
let mut literal_counts = [1_usize; 256];
for sample in samples {
let sample = sample.as_ref();
let remaining = options.max_training_bytes.saturating_sub(training.len());
let retained = &sample[..sample.len().min(remaining)];
for &byte in retained {
literal_counts[usize::from(byte)] = literal_counts[usize::from(byte)].saturating_add(1);
}
training.extend_from_slice(retained);
if training.len() == options.max_training_bytes {
break;
}
}
if training.is_empty() {
return Err(DictionaryTrainingError::EmptySamples);
}
let huffman = HuffmanTable::build_from_counts(&literal_counts);
let mut metadata = Vec::new();
metadata.extend_from_slice(&MAGIC_NUM);
metadata.extend_from_slice(&options.dictionary_id.to_le_bytes());
metadata.extend_from_slice(huffman.table_description());
append_fse_description(&mut metadata, default_of_table());
append_fse_description(&mut metadata, default_ml_table());
append_fse_description(&mut metadata, default_ll_table());
let minimum_content = 8;
let Some(content_budget) = options
.dictionary_size
.checked_sub(metadata.len().saturating_add(12))
.filter(|budget| *budget >= minimum_content)
else {
return Err(DictionaryTrainingError::DictionaryTooSmall);
};
let mut content = Vec::new();
create_raw_dict_from_source(
training.as_slice(),
training.len(),
&mut content,
content_budget,
);
if content.len() < minimum_content {
let missing = minimum_content - content.len();
content.extend(training.iter().copied().cycle().take(missing));
}
content.truncate(content_budget);
metadata.extend_from_slice(&1_u32.to_le_bytes());
metadata.extend_from_slice(&(content.len().min(4) as u32).to_le_bytes());
metadata.extend_from_slice(&(content.len().min(8) as u32).to_le_bytes());
metadata.extend_from_slice(&content);
Ok(metadata)
}
fn append_fse_description(output: &mut Vec<u8>, table: crate::fse::fse_encoder::FSETable) {
let mut writer = BitWriter::new();
table.write_table(&mut writer).expect("owned dictionary table allocation failed");
output.extend_from_slice(&writer.dump().expect("owned dictionary output allocation failed"));
}
pub(super) struct DictParams {
pub segment_size: u32,
}
pub fn create_raw_dict_from_dir<P: AsRef<Path>, W: io::Write>(
path: P,
output: &mut W,
dict_size: usize,
) -> Result<(), io::Error> {
let mut file_paths: Vec<PathBuf> = Vec::new();
let dir: fs::ReadDir = fs::read_dir(path)?;
fn recurse_read(dir: fs::ReadDir, file_paths: &mut Vec<PathBuf>) -> Result<(), io::Error> {
for entry in dir {
let entry = entry?;
if entry.file_type()?.is_dir() {
recurse_read(fs::read_dir(entry.path())?, file_paths)?;
} else {
file_paths.push(entry.path());
}
}
Ok(())
}
recurse_read(dir, &mut file_paths)?;
let mut total_file_len: u64 = 0;
let mut file_handles: Vec<fs::File> = Vec::new();
for path in file_paths {
let handle = File::open(path)?;
total_file_len += handle.metadata()?.len();
file_handles.push(handle);
}
let empty_reader: Box<dyn Read> = Box::new(io::empty());
let chained_files = file_handles
.iter()
.fold(empty_reader, |acc, reader| Box::new(acc.chain(reader)));
create_raw_dict_from_source(chained_files, total_file_len as usize, output, dict_size);
Ok(())
}
pub fn create_raw_dict_from_source<R: io::Read, W: io::Write>(
mut source: R,
source_size: usize,
output: &mut W,
dict_size: usize,
) {
if source_size < 16 {
let mut source = source;
let mut buf = Vec::new();
source
.read_to_end(&mut buf)
.expect("Could not read from source");
output.write_all(&buf).expect("Could not write to output");
return;
}
vprintln!("create_dict: creating {dict_size} byte dict from {source_size} byte source");
if dict_size == 0 {
return;
}
let mut source_data = Vec::with_capacity(source_size);
source
.read_to_end(&mut source_data)
.expect("can read input");
if source_data.is_empty() {
return;
}
if source_data.len() < K {
output
.write_all(&source_data[..usize::min(dict_size, source_data.len())])
.expect("can write to output");
return;
}
let params = DictParams { segment_size: 2048 };
let segment_size = params.segment_size as usize;
let num_segments = usize::max(1, source_data.len().div_ceil(segment_size));
let sample_divisor = usize::min(usize::max(1, source_data.len() / (2 * num_segments)), 256);
let sample_size = usize::max(K, source_data.len() / sample_divisor);
vprintln!("create_dict: creating {sample_size} byte sample of collection");
let collection_sample = create_sample(&mut source_data.as_slice(), sample_size);
let mut pool: BinaryHeap<Reverse<Segment>> = BinaryHeap::new();
let (_, epoch_kmers) = compute_epoch_info(¶ms, dict_size, source_data.len() / K);
let epoch_size = usize::min(source_data.len(), usize::max(K, epoch_kmers * K));
let num_epochs = source_data.len().div_ceil(epoch_size);
vprintln!("create_dict: computed epoch info, using {num_epochs} epochs of {epoch_size} bytes");
let mut ctx = Context {
frequencies: HashMap::with_capacity(epoch_size / K),
};
for (epoch_index, current_epoch) in source_data.chunks(epoch_size).enumerate() {
let Some(best_segment) =
pick_best_segment(¶ms, &mut ctx, current_epoch, &collection_sample)
else {
continue;
};
vprintln!(
"\tcreate_dict: epoch {}/{} has best segment score {}",
epoch_index + 1,
num_epochs,
best_segment.score
);
pool.push(Reverse(best_segment));
ctx.frequencies.clear();
}
vprintln!(
"create_dict: {num_epochs} epochs written, writing {} segments",
pool.len()
);
let mut remaining = dict_size;
while let Some(segment) = pool.pop() {
if remaining == 0 {
break;
}
let bytes_to_write = usize::min(remaining, segment.0.raw.len());
output
.write_all(&segment.0.raw[..bytes_to_write])
.expect("can write to output");
remaining -= bytes_to_write;
}
}
#[cfg(test)]
mod tests {
use super::{
create_raw_dict_from_source, train_dictionary, DictionaryTrainerOptions,
DictionaryTrainingError,
};
use std::{io::Read, vec::Vec};
#[test]
fn raw_dict_builder_scores_epochs_after_sampling() {
let mut source = Vec::new();
for _ in 0..512 {
source.extend_from_slice(b"aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb");
source.extend_from_slice(b"ccccccccccccccccdddddddddddddddd");
}
let mut dictionary = Vec::new();
create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
assert!(!dictionary.is_empty());
assert!(dictionary.len() <= 1024);
}
#[test]
fn raw_dict_builder_handles_tiny_sources() {
let source = b"tiny";
let mut dictionary = Vec::new();
create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
assert_eq!(dictionary, source);
}
#[test]
fn formatted_dictionary_is_usable_by_encoder_and_decoder() {
let samples = [
b"customer=alice action=login status=success".repeat(100),
b"customer=bob action=logout status=success".repeat(100),
];
let dictionary =
train_dictionary(&samples, DictionaryTrainerOptions::new(2048, 0xC0DE)).unwrap();
let decoded = crate::decoding::Dictionary::decode_dict(&dictionary).unwrap();
assert_eq!(decoded.id, 0xC0DE);
assert!(dictionary.len() <= 2048);
let input = b"customer=alice action=logout status=success".repeat(200);
let encoder_dictionary = crate::encoding::EncoderDictionary::copy(&dictionary).unwrap();
let compressed = crate::encoding::encode_all(
input.as_slice(),
crate::encoding::EncoderOptions::default().with_dictionary(encoder_dictionary),
)
.unwrap();
let mut c_decoder =
zstd::stream::read::Decoder::with_dictionary(compressed.as_slice(), &dictionary)
.unwrap();
let mut output = Vec::new();
c_decoder.read_to_end(&mut output).unwrap();
assert_eq!(output, input);
}
#[test]
fn formatted_dictionary_options_are_validated() {
assert_eq!(
train_dictionary(&[b"sample"], DictionaryTrainerOptions::new(1024, 0)),
Err(DictionaryTrainingError::ZeroDictionaryId)
);
assert_eq!(
train_dictionary(&[b"sample"], DictionaryTrainerOptions::new(8, 1)),
Err(DictionaryTrainingError::DictionaryTooSmall)
);
}
}
#[test]
fn create_raw_dict_from_source_no_panics_on_small_input() {
use std::io::Cursor;
for size in 0..1024 {
let input = alloc::vec![b'A'; size];
let mut output = Vec::new();
create_raw_dict_from_source(Cursor::new(input.clone()), input.len(), &mut output, 64);
}
}