use alloc::vec::Vec;
use crate::Backend;
use crate::shared::varint;
#[derive(Debug)]
pub(crate) struct ZstdNode {
encoder: mzstd::Compressor,
output: Vec<u8>,
config: mzstd::EncoderConfig,
}
const MAX_BLOCK_ELTS: usize = 8;
const MIN_BLOCK_WIDTH: usize = 1024;
pub(crate) fn zstd_backend(backend: Backend) -> mzstd::Backend {
mzstd::Backend::available()
.into_iter()
.find(|candidate| candidate.name() == backend.name())
.unwrap_or_default()
}
pub(crate) fn zstd_config(level: i32) -> mzstd::EncoderConfig {
let level = mzstd::Level::try_from(level).unwrap_or_default();
mzstd::EncoderConfig::default()
.with_format(mzstd::FrameFormat::Magicless)
.with_level(level)
}
const fn should_cut_blocks(width: usize, n_elts: usize) -> bool {
n_elts > 0 && width >= MIN_BLOCK_WIDTH && n_elts <= MAX_BLOCK_ELTS
}
impl ZstdNode {
pub(crate) fn new(level: i32, backend: Backend) -> Self {
let encoder = mzstd::Compressor::builder(zstd_config(level))
.with_backend(zstd_backend(backend))
.build()
.unwrap_or_default();
Self {
encoder,
output: Vec::new(),
config: zstd_config(level),
}
}
pub(crate) fn set_level(&mut self, level: i32) {
let _ = self.set_config(zstd_config(level));
}
pub(crate) fn set_config(&mut self, config: mzstd::EncoderConfig) -> Result<(), ()> {
if config != self.config {
self.encoder.reconfigure(config).map_err(|_| ())?;
self.config = config;
}
Ok(())
}
#[cfg_attr(all(feature = "hotpath", not(feature = "no_std")), hotpath::measure)]
pub(crate) fn encode(&mut self, input: &[u8]) -> Option<&[u8]> {
self.output.clear();
self.output.push(1);
self.encoder.compress_into(input, &mut self.output).ok()?;
Some(&self.output)
}
#[cfg_attr(all(feature = "hotpath", not(feature = "no_std")), hotpath::measure)]
pub(crate) fn encode_elements(
&mut self,
input: &[u8],
width: usize,
n_elts: usize,
dictionary: Option<&mzstd::dictionary::PreparedDictionary>,
) -> Option<&[u8]> {
self.output.clear();
varint::push(&mut self.output, width as u64);
if !should_cut_blocks(width, n_elts) || n_elts == 1 {
match dictionary {
Some(dictionary) => self
.encoder
.compress_with_dictionary_into(dictionary, input, &mut self.output)
.ok()?,
None => self.encoder.compress_into(input, &mut self.output).ok()?,
};
return Some(&self.output);
}
let header = self.output.len();
let capacity = mzstd::Compressor::max_compressed_size(input.len())
.ok()?
.checked_add(n_elts * 3)?;
self.output.resize(header.checked_add(capacity)?, 0);
let mut session = self
.encoder
.start(mzstd::InputSize::Exact(input.len() as u64).into());
let mut position = header;
for (index, element) in input.chunks_exact(width).enumerate() {
let operation = if index + 1 == n_elts {
mzstd::Operation::End
} else {
mzstd::Operation::Flush
};
let progress = session
.process(element, &mut self.output[position..], operation)
.ok()?;
if progress.consumed != element.len() {
return None;
}
position += progress.produced;
if operation == mzstd::Operation::End
&& progress.status != mzstd::EncoderStatus::Finished
{
return None;
}
}
self.output.truncate(position);
Some(&self.output)
}
pub(crate) fn retained_bytes(&self) -> usize {
self.output.capacity() + self.encoder.retained_bytes()
}
pub(crate) fn trim(&mut self, policy: crate::RetentionPolicy) {
if !policy.allows(self.retained_bytes()) {
self.output = Vec::new();
self.encoder.trim(mzstd::RetentionPolicy::ReleaseAll);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn magicless_frame(input: &[u8], level: i32) -> Vec<u8> {
let level = mzstd::Level::try_from(level).expect("level");
let config = mzstd::EncoderConfig::default().with_level(level);
let mut reference = mzstd::Compressor::new(config).expect("zstd config");
reference.compress(input).expect("zstd compress")[4..].to_vec()
}
#[test]
fn stream_is_the_width_then_the_magicless_frame() {
let input = alloc::vec![7u8; 4096];
let mut node = ZstdNode::new(6, Backend::default());
let stream = node.encode(&input).expect("encode").to_vec();
assert_eq!(stream[0], 1);
assert_eq!(&stream[1..], &magicless_frame(&input, 6)[..]);
assert_eq!(
node.encode_elements(&input, 1, input.len(), None).unwrap(),
&stream[..]
);
}
#[test]
fn a_single_wide_element_is_one_frame_after_its_width() {
let input = alloc::vec![9u8; 3000];
let mut node = ZstdNode::new(3, Backend::default());
let stream = node
.encode_elements(&input, 3000, 1, None)
.unwrap()
.to_vec();
assert_eq!(&stream[..2], &[0xb8, 0x17]);
assert_eq!(&stream[2..], &magicless_frame(&input, 3)[..]);
}
#[test]
fn wide_elements_get_one_block_each() {
let width = 2048;
let input: Vec<u8> = (0..(width * 3) as u32)
.map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
.collect();
let mut node = ZstdNode::new(3, Backend::default());
let stream = node
.encode_elements(&input, width, 3, None)
.unwrap()
.to_vec();
let mut decoder = mzstd::Decompressor::builder(
mzstd::DecoderConfig::default().with_format(mzstd::FrameFormat::Magicless),
)
.build();
let mut expected = alloc::vec::Vec::new();
crate::shared::varint::push(&mut expected, width as u64);
let len = expected.len();
assert_eq!(&stream[..len], expected.as_slice());
assert_eq!(decoder.decompress(&stream[len..]).unwrap(), input);
assert_ne!(&stream[len..], &magicless_frame(&input, 3)[..]);
let one_call = node
.encode_elements(&input, 512, 12, None)
.unwrap()
.to_vec();
assert_eq!(&one_call[2..], &magicless_frame(&input, 3)[..]);
}
#[test]
fn empty_input_is_an_empty_frame() {
let mut node = ZstdNode::new(3, Backend::default());
let stream = node.encode_elements(&[], 4096, 0, None).unwrap().to_vec();
assert_eq!(&stream[2..], &magicless_frame(&[], 3)[..]);
}
#[test]
fn level_changes_apply_to_the_next_chunk() {
let input: alloc::vec::Vec<u8> = (0..20_000u32)
.map(|i| b"abcdefgh"[(i.wrapping_mul(2_654_435_761) >> 29) as usize])
.collect();
let mut node = ZstdNode::new(1, Backend::default());
let fast = node.encode(&input).unwrap().to_vec();
node.set_level(19);
let strong = node.encode(&input).unwrap().to_vec();
assert_ne!(fast, strong);
assert!(node.retained_bytes() > 0);
node.trim(crate::RetentionPolicy::ReleaseAll);
assert_eq!(node.output.capacity(), 0);
}
#[test]
fn configurations_that_do_not_validate_are_refused() {
let mut node = ZstdNode::new(3, Backend::default());
let params = mzstd::CompressionParams::default().with_window_log(5);
assert!(node.set_config(zstd_config(3).with_params(params)).is_err());
assert!(node.encode(b"still working fine").is_some());
}
#[cfg(not(miri))]
fn mixed(len: usize, seed: u32) -> Vec<u8> {
(0..len as u32)
.map(|i| {
let h = (i ^ seed).wrapping_mul(2_654_435_761) >> 20;
if h.is_multiple_of(5) {
h as u8
} else {
b"abcdefgh"[(h & 7) as usize]
}
})
.collect()
}
#[cfg(not(miri))]
#[test]
fn elements_match_the_reference_binding_at_every_level() {
let shapes = [
(1, 5000),
(1024, 1),
(1024, 2),
(2048, 3),
(1500, 8),
(1024, 9),
(4096, 8),
(512, 12),
(4096, 0),
(70_000, 3),
];
for level in [-5, -1, 0, 1, 3, 6, 9, 12, 19, 22] {
let mut node = ZstdNode::new(level, Backend::default());
for (width, n_elts) in shapes {
let input = mixed(width * n_elts, (width + n_elts) as u32);
let expected = openzl_ffi::enc_zstd::zstd_node(&input, width, level, 3, &[]);
let actual = node
.encode_elements(&input, width, n_elts, None)
.map(<[u8]>::to_vec);
assert_eq!(actual, expected, "level {level} width {width} x{n_elts}");
}
}
}
#[cfg(not(miri))]
#[test]
fn literal_mode_and_parameters_match_the_reference_binding() {
use mzstd::CParameter;
let input = mixed(3 * 2048, 7);
let mut node = ZstdNode::new(3, Backend::default());
for ints in [
&[][..],
&[(100, 7), (101, 15), (107, 4)][..],
&[(1002, 2)][..],
&[(201, 1), (202, 1), (160, 1)][..],
] {
let mut config = zstd_config(3);
for &(id, value) in ints {
config = config.with_parameter(CParameter::try_from((id, value)).unwrap());
}
node.set_config(config).unwrap();
let expected = openzl_ffi::enc_zstd::zstd_node(&input, 2048, 3, 3, ints);
let actual = node
.encode_elements(&input, 2048, 3, None)
.map(<[u8]>::to_vec);
assert_eq!(actual, expected, "{ints:?}");
}
}
#[test]
fn backends_map_to_the_same_named_zstd_backend() {
for backend in Backend::available() {
let mapped = zstd_backend(backend);
if backend != Backend::SCALAR {
assert_eq!(mapped.name(), backend.name());
}
}
}
}