use super::intpack::{IntpackReader, pack_u64s};
use super::txt_streams::{build_canonical, malformed, write_container};
use crate::bytes::le_u64;
use crate::error::UrnaError;
use zstd::bulk::{Compressor, Decompressor};
use zstd::dict::{DecoderDictionary, EncoderDictionary};
pub const TXT_STREAMS_V2: u8 = 1;
const DICT_LEVEL: i32 = super::zstd_codec::DEFAULT_ZSTD_LEVEL;
pub const MAX_DICT_BYTES: usize = 112 * 1024;
const STREAM_CAP: usize = 64 * 1024 * 1024;
pub fn train_dict(sorted_unique: &[String]) -> Option<Vec<u8>> {
if sorted_unique.len() < 8 {
return None;
}
let total: usize = sorted_unique.iter().map(|s| s.len()).sum();
if total == 0 {
return None;
}
let mut data: Vec<u8> = Vec::with_capacity(total);
let mut sizes: Vec<usize> = Vec::with_capacity(sorted_unique.len());
for s in sorted_unique {
data.extend_from_slice(s.as_bytes());
sizes.push(s.len());
}
let cap = (total / 10).clamp(4 * 1024, MAX_DICT_BYTES);
zstd::dict::from_continuous(&data, &sizes, cap).ok()
}
pub fn encode(texts: &[String], dict: &[u8]) -> crate::Result<Vec<u8>> {
let edict = EncoderDictionary::copy(dict, DICT_LEVEL);
let mut comp = Compressor::with_prepared_dictionary(&edict)
.map_err(|e| UrnaError::InvalidInput(format!("zstd_dict: compressor init: {}", e)))?;
let mut streams: Vec<u8> = Vec::new();
let mut offsets: Vec<u64> = Vec::with_capacity(texts.len() + 1);
offsets.push(0);
for t in texts {
let c = comp
.compress(t.as_bytes())
.map_err(|e| UrnaError::InvalidInput(format!("zstd_dict: compress: {}", e)))?;
streams.extend_from_slice(&c);
offsets.push(streams.len() as u64);
}
let table = pack_u64s(&offsets);
Ok(write_container(
TXT_STREAMS_V2,
texts.len(),
&table,
&streams,
))
}
fn decompress_stream(dec: &mut Decompressor<'_>, stream: &[u8]) -> crate::Result<Vec<u8>> {
let cap = Decompressor::upper_bound(stream)
.unwrap_or(STREAM_CAP)
.min(STREAM_CAP);
let raw = dec
.decompress(stream, cap)
.map_err(|e| malformed(format!("zstd_dict: decompress: {}", e)))?;
std::str::from_utf8(&raw).map_err(|e| malformed(format!("zstd_dict: invalid utf-8: {}", e)))?;
Ok(raw)
}
pub fn decode(bytes: &[u8], dict: &[u8]) -> crate::Result<Vec<u8>> {
let (count, offsets, streams) = parse_v2(bytes)?;
let ddict = DecoderDictionary::copy(dict);
let mut dec = Decompressor::with_prepared_dictionary(&ddict)
.map_err(|e| UrnaError::InvalidInput(format!("zstd_dict: decompressor init: {}", e)))?;
let mut bodies: Vec<Vec<u8>> = Vec::with_capacity(count);
for i in 0..count {
let s = stream_slice(streams, &offsets, i)?;
bodies.push(decompress_stream(&mut dec, s)?);
}
build_canonical(count, &bodies)
}
fn parse_v2(bytes: &[u8]) -> crate::Result<(usize, Vec<u64>, &[u8])> {
let (kind, rest) = bytes
.split_first()
.ok_or_else(|| malformed("zstd_dict: empty"))?;
if *kind != TXT_STREAMS_V2 {
return Err(malformed(format!("zstd_dict: unknown kind {}", *kind)));
}
if rest.len() < 8 {
return Err(malformed("zstd_dict: truncated count"));
}
let declared = le_u64(&rest[0..8])?;
let table_bytes = &rest[8..];
let reader = IntpackReader::parse(table_bytes)?;
if reader.is_empty() {
return Err(malformed("zstd_dict: offset table must hold n+1 >= 1"));
}
let count = reader.len() - 1;
if declared != count as u64 {
return Err(malformed("zstd_dict: declared count != offset count - 1"));
}
let mut offsets = Vec::with_capacity(reader.len());
for i in 0..reader.len() {
offsets.push(reader.get(i)?);
}
if offsets[0] != 0 {
return Err(malformed("zstd_dict: first offset must be 0"));
}
let table_len = pack_u64s(&offsets).len();
if table_len > table_bytes.len() {
return Err(malformed("zstd_dict: truncated offset table"));
}
let streams = &table_bytes[table_len..];
for w in offsets.windows(2) {
if w[1] < w[0] {
return Err(malformed("zstd_dict: non-monotonic offsets"));
}
}
if offsets.last().copied() != Some(streams.len() as u64) {
return Err(malformed("zstd_dict: final offset != streams length"));
}
Ok((count, offsets, streams))
}
#[inline]
fn stream_slice<'a>(streams: &'a [u8], offsets: &[u64], i: usize) -> crate::Result<&'a [u8]> {
let start = offsets[i] as usize;
let end = offsets[i + 1] as usize;
streams
.get(start..end)
.ok_or_else(|| malformed("zstd_dict: stream slice out of bounds"))
}