use super::config::{ConfigError, EncoderConfig, SizeOverflow};
use super::core::bound::bound;
use super::core::driver::{
EncoderCache, compress_to_slice_attached, compress_to_vec_attached, quality_reads_a_prefix,
};
use super::dictionary::PreparedDictionary;
use super::error::EncodeError;
use super::internal::{CompressParams, QualityLevel, WindowBits};
use super::session::{EncoderSession, StreamConfig};
use fearless_simd::Level;
use std::ops::Range;
const WIDEST_BOUND: CompressParams = CompressParams::new(QualityLevel::Q0, WindowBits::MIN);
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub enum RetentionPolicy {
#[default]
Aggressive,
CurrentConfig,
Bounded {
max_bytes: usize,
},
ReleaseAll,
}
#[derive(Debug)]
pub struct Compressor {
level: Level,
config: EncoderConfig,
retention: RetentionPolicy,
pub(crate) workspace: EncoderCache,
pub(crate) staging: Vec<u8>,
pub(crate) pending: Vec<u8>,
pub(crate) served: usize,
pub(crate) active: bool,
}
impl Compressor {
#[inline]
pub fn new(config: EncoderConfig) -> Result<Self, ConfigError> {
Self::builder(config).build()
}
#[must_use]
pub const fn builder(config: EncoderConfig) -> CompressorBuilder {
CompressorBuilder {
config,
retention: RetentionPolicy::Aggressive,
level: None,
}
}
#[must_use]
pub const fn config(&self) -> &EncoderConfig {
&self.config
}
#[must_use]
pub const fn retention(&self) -> RetentionPolicy {
self.retention
}
pub fn reconfigure(&mut self, config: EncoderConfig) -> Result<(), ConfigError> {
config.validate()?;
let changed = self.config != config;
self.config = config;
self.reset_stream_state();
if changed && matches!(self.retention, RetentionPolicy::CurrentConfig) {
self.workspace.invalidate();
}
Ok(())
}
pub fn max_compressed_size(input_size: usize) -> Result<usize, SizeOverflow> {
match bound(&WIDEST_BOUND, input_size) {
Ok(bound) => Ok(bound),
Err(_) => Err(SizeOverflow),
}
}
#[inline]
pub fn compress(&mut self, src: &[u8]) -> Result<Vec<u8>, EncodeError> {
let mut output = Vec::new();
self.compress_into(src, &mut output)?;
Ok(output)
}
#[inline]
pub fn compress_into(
&mut self,
src: &[u8],
dst: &mut Vec<u8>,
) -> Result<Range<usize>, EncodeError> {
self.compress_attached_into(None, src, dst)
}
#[inline]
pub fn compress_to_slice(&mut self, src: &[u8], dst: &mut [u8]) -> Result<usize, EncodeError> {
self.compress_attached_to_slice(None, src, dst)
}
pub fn compress_with_dictionary(
&mut self,
dictionary: &PreparedDictionary,
src: &[u8],
) -> Result<Vec<u8>, EncodeError> {
let mut output = Vec::new();
self.compress_with_dictionary_into(dictionary, src, &mut output)?;
Ok(output)
}
pub fn compress_with_dictionary_into(
&mut self,
dictionary: &PreparedDictionary,
src: &[u8],
dst: &mut Vec<u8>,
) -> Result<Range<usize>, EncodeError> {
self.compress_attached_into(Some(dictionary), src, dst)
}
pub fn compress_with_dictionary_to_slice(
&mut self,
dictionary: &PreparedDictionary,
src: &[u8],
dst: &mut [u8],
) -> Result<usize, EncodeError> {
self.compress_attached_to_slice(Some(dictionary), src, dst)
}
pub fn start(
&mut self,
stream: StreamConfig,
) -> Result<EncoderSession<'_, 'static>, EncodeError> {
let limit = self.begin(None, stream)?;
Ok(EncoderSession::new(self, None, limit, stream))
}
pub fn start_with_dictionary<'c, 'd>(
&'c mut self,
dictionary: &'d PreparedDictionary,
stream: StreamConfig,
) -> Result<EncoderSession<'c, 'd>, EncodeError> {
let limit = self.begin(Some(dictionary), stream)?;
Ok(EncoderSession::new(self, Some(dictionary), limit, stream))
}
#[must_use]
pub fn retained_bytes(&self) -> usize {
self.workspace.retained_bytes() + self.staging.capacity() + self.pending.capacity()
}
pub fn trim(&mut self, policy: RetentionPolicy) {
let release = match policy {
RetentionPolicy::Aggressive | RetentionPolicy::CurrentConfig => false,
RetentionPolicy::Bounded { max_bytes } => self.retained_bytes() > max_bytes,
RetentionPolicy::ReleaseAll => true,
};
if release {
self.workspace.invalidate();
self.staging = Vec::new();
self.pending = Vec::new();
self.served = 0;
}
}
pub fn recover(&mut self) {
self.workspace.invalidate();
self.reset_stream_state();
}
#[must_use]
pub fn fork_empty(&self) -> Self {
Self {
level: self.level,
config: self.config,
retention: self.retention,
workspace: EncoderCache::default(),
staging: Vec::new(),
pending: Vec::new(),
served: 0,
active: false,
}
}
fn compress_attached_into(
&mut self,
dictionary: Option<&PreparedDictionary>,
src: &[u8],
dst: &mut Vec<u8>,
) -> Result<Range<usize>, EncodeError> {
self.ensure_available()?;
if dictionary.is_some() {
self.check_dictionary()?;
}
let start = dst.len();
let params = self.config.lower(Some(src.len()));
let reserve = bound(¶ms, src.len()).map_err(|_| SizeOverflow)?;
dst.try_reserve(reserve)
.map_err(|_| EncodeError::AllocationFailed { requested: reserve })?;
let attached = dictionary.map(PreparedDictionary::inner);
let outcome =
compress_to_vec_attached(&mut self.workspace, self.level, ¶ms, attached, src, dst);
self.finish_operation();
match outcome {
Ok(()) => Ok(start..dst.len()),
Err(error) => {
dst.truncate(start);
Err(EncodeError::from_core(error, 0))
}
}
}
fn compress_attached_to_slice(
&mut self,
dictionary: Option<&PreparedDictionary>,
src: &[u8],
dst: &mut [u8],
) -> Result<usize, EncodeError> {
self.ensure_available()?;
if dictionary.is_some() {
self.check_dictionary()?;
}
let params = self.config.lower(Some(src.len()));
let attached = dictionary.map(PreparedDictionary::inner);
let provided = dst.len();
let outcome = compress_to_slice_attached(
&mut self.workspace,
self.level,
¶ms,
attached,
src,
dst,
);
self.finish_operation();
outcome.map_err(|error| EncodeError::from_core(error, provided))
}
fn begin(
&mut self,
dictionary: Option<&PreparedDictionary>,
stream: StreamConfig,
) -> Result<usize, EncodeError> {
self.ensure_available()?;
if stream.stream_offset() != 0
&& (!cfg!(feature = "experimental") || self.config.quality().get() < 2)
{
return Err(EncodeError::UnsupportedStreamOffset {
offset: stream.stream_offset(),
});
}
if dictionary.is_some() {
self.check_dictionary()?;
}
let params = self.config.lower(Some(stream.input_size().hint()));
#[cfg(feature = "experimental")]
let params = {
let input_bytes = match stream.input_size() {
super::InputSize::Exact(size) => size,
super::InputSize::Unknown => 0,
};
if stream
.stream_offset()
.checked_add(input_bytes)
.is_none_or(|end| end > (1u64 << 63) - 1)
{
return Err(EncodeError::StreamPositionOverflow {
position: stream.stream_offset(),
input_bytes,
});
}
let mut params = params;
let bits = self.config.window().bits().min(30);
params.stream_offset = stream.stream_offset().min((1u64 << bits) - 16) as usize;
params
};
let limit = match self.workspace.acquire(self.level, ¶ms, 0) {
Ok(encoder) => encoder.block_size_limit(),
Err(error) => {
self.workspace.invalidate();
return Err(EncodeError::from_core(error, 0));
}
};
self.staging.clear();
self.pending.clear();
self.served = 0;
if self.staging.capacity() < limit {
self.staging
.try_reserve(limit)
.map_err(|_| EncodeError::AllocationFailed { requested: limit })?;
}
self.active = true;
Ok(limit)
}
const fn ensure_available(&self) -> Result<(), EncodeError> {
if self.active {
return Err(EncodeError::AbandonedSession);
}
Ok(())
}
const fn check_dictionary(&self) -> Result<(), EncodeError> {
if quality_reads_a_prefix(self.config.quality().level()) {
return Ok(());
}
Err(EncodeError::DictionaryUnsupportedForQuality {
quality: self.config.quality(),
})
}
pub(crate) fn finish_operation(&mut self) {
self.trim(self.retention);
}
fn reset_stream_state(&mut self) {
self.staging.clear();
self.pending.clear();
self.served = 0;
self.active = false;
}
pub(crate) const fn has_pending(&self) -> bool {
self.served < self.pending.len()
}
}
#[derive(Debug)]
pub struct CompressorBuilder {
config: EncoderConfig,
retention: RetentionPolicy,
level: Option<Level>,
}
impl CompressorBuilder {
#[must_use]
pub const fn with_retention(mut self, retention: RetentionPolicy) -> Self {
self.retention = retention;
self
}
#[must_use]
pub const fn with_backend(mut self, backend: super::Backend) -> Self {
self.level = Some(backend.0);
self
}
#[inline]
pub fn build(self) -> Result<Compressor, ConfigError> {
self.config.validate()?;
Ok(Compressor {
level: self
.level
.unwrap_or_else(|| Level::try_detect().unwrap_or_else(Level::baseline)),
config: self.config,
retention: self.retention,
workspace: EncoderCache::default(),
staging: Vec::new(),
pending: Vec::new(),
served: 0,
active: false,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compressor::config::{Quality, Window};
fn compressor(quality: Quality) -> Compressor {
Compressor::new(EncoderConfig::default().with_quality(quality)).expect("a legal config")
}
#[test]
fn a_compressor_is_send_and_movable() {
const fn assert_send<T: Send>() {}
assert_send::<Compressor>();
assert_send::<CompressorBuilder>();
let mut moved = std::thread::spawn(|| {
let mut encoder = compressor(Quality::Q1);
encoder.compress(b"payload payload").expect("compressed")
})
.join()
.expect("the worker finished");
assert!(!moved.is_empty());
moved.clear();
}
#[test]
fn the_widest_bound_covers_every_configuration() {
for quality in 0u8..=11 {
for bits in [10u8, 16, 22, 24] {
let config = EncoderConfig::default()
.with_quality(Quality::try_from(quality).expect("legal"))
.with_window(Window::standard(bits).expect("legal"));
for size in [0usize, 1, 1024, 1 << 16] {
let specific =
bound(&config.lower(Some(size)), size).expect("a representable bound");
let widest = Compressor::max_compressed_size(size).expect("representable");
assert!(
widest >= specific,
"q{quality} w{bits} at {size} bytes: {widest} < {specific}"
);
}
}
}
assert!(Compressor::max_compressed_size(usize::MAX).is_err());
}
#[test]
fn a_retention_policy_releases_what_it_says_it_will() {
let mut encoder = compressor(Quality::Q5);
encoder.compress(b"payload payload payload").expect("ok");
let warm = encoder.retained_bytes();
assert!(warm > 0);
encoder.trim(RetentionPolicy::Aggressive);
assert_eq!(encoder.retained_bytes(), warm);
encoder.trim(RetentionPolicy::CurrentConfig);
assert_eq!(encoder.retained_bytes(), warm);
encoder.trim(RetentionPolicy::Bounded {
max_bytes: usize::MAX,
});
assert_eq!(encoder.retained_bytes(), warm);
encoder.trim(RetentionPolicy::Bounded { max_bytes: 0 });
assert_eq!(encoder.retained_bytes(), 0);
encoder.compress(b"payload payload payload").expect("ok");
assert!(encoder.retained_bytes() > 0);
encoder.trim(RetentionPolicy::ReleaseAll);
assert_eq!(encoder.retained_bytes(), 0);
}
#[test]
fn the_release_all_policy_retains_nothing_across_calls() {
let mut encoder = Compressor::builder(EncoderConfig::default().with_quality(Quality::Q5))
.with_retention(RetentionPolicy::ReleaseAll)
.build()
.expect("legal");
let first = encoder.compress(b"payload payload payload").expect("ok");
assert_eq!(encoder.retained_bytes(), 0);
assert_eq!(
encoder.compress(b"payload payload payload").expect("ok"),
first
);
}
#[test]
fn reconfiguring_to_the_same_shape_keeps_the_workspace() {
let mut encoder = compressor(Quality::Q5);
encoder.compress(b"payload payload payload").expect("ok");
let warm = encoder.retained_bytes();
encoder
.reconfigure(EncoderConfig::default().with_quality(Quality::Q5))
.expect("legal");
assert_eq!(encoder.retained_bytes(), warm);
}
#[test]
fn the_current_config_policy_releases_on_a_real_change() {
let mut encoder = Compressor::builder(EncoderConfig::default().with_quality(Quality::Q5))
.with_retention(RetentionPolicy::CurrentConfig)
.build()
.expect("legal");
encoder.compress(b"payload payload payload").expect("ok");
assert!(encoder.retained_bytes() > 0);
encoder
.reconfigure(EncoderConfig::default().with_quality(Quality::Q5))
.expect("legal");
assert!(encoder.retained_bytes() > 0, "an identical config released");
encoder
.reconfigure(EncoderConfig::default().with_quality(Quality::Q1))
.expect("legal");
assert_eq!(encoder.retained_bytes(), 0);
}
#[test]
fn a_dictionary_is_refused_below_the_quality_that_can_read_one() {
for quality in 0u8..=11 {
let encoder = compressor(Quality::try_from(quality).expect("legal"));
let outcome = encoder.check_dictionary();
if quality >= 5 {
assert!(outcome.is_ok(), "q{quality} refused a dictionary");
} else {
assert!(
matches!(
outcome,
Err(EncodeError::DictionaryUnsupportedForQuality { .. })
),
"q{quality} accepted a dictionary"
);
}
}
}
#[test]
fn a_forked_compressor_copies_the_settings_and_none_of_the_buffers() {
let mut encoder = Compressor::builder(EncoderConfig::default().with_quality(Quality::Q5))
.with_retention(RetentionPolicy::ReleaseAll)
.build()
.expect("legal");
encoder.staging.extend_from_slice(&[0u8; 64]);
let forked = encoder.fork_empty();
assert_eq!(forked.config(), encoder.config());
assert_eq!(forked.retention(), RetentionPolicy::ReleaseAll);
assert_eq!(forked.retained_bytes(), 0);
}
}