#[allow(unused_imports)]
use std::io::Write;
#[allow(unused_imports)]
use crate::Result;
#[allow(unused_imports)]
use super::options::WriteOptions;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Concurrency {
Alongside,
Alone(usize),
}
impl Concurrency {
#[cfg_attr(
not(all(feature = "lzma2", feature = "parallel")),
allow(unused_variables)
)]
pub(crate) fn alone(options: &WriteOptions, _data_len: usize) -> Self {
#[cfg(all(feature = "lzma2", feature = "parallel"))]
{
Self::Alone(options.threads.count())
}
#[cfg(not(all(feature = "lzma2", feature = "parallel")))]
Self::Alone(1)
}
#[cfg(feature = "async")]
pub(crate) fn for_entry(options: &WriteOptions, data_len: usize) -> Self {
let large = data_len as u64 >= super::streaming_entry::STREAMING_THRESHOLD;
if large && super::streaming_entry::can_stream(options) {
Self::alone(options, data_len)
} else {
Self::Alongside
}
}
#[cfg(all(feature = "lzma2", feature = "parallel"))]
fn is_chunked(
self,
options: &WriteOptions,
encoder: &crate::codec::lzma::Lzma2EncoderOptions,
data_len: usize,
) -> bool {
match self {
Self::Alongside => false,
Self::Alone(_) => {
let dictionary = u64::from(encoder.dict_size.unwrap_or(0));
data_len as u64 >= crate::codec::lzma2_chunked::shortest_split_stream(dictionary)
&& lzma2_is_chunked(options, encoder)
}
}
}
#[cfg(all(feature = "lzma2", feature = "parallel"))]
fn workers(self) -> usize {
match self {
Self::Alongside => 1,
Self::Alone(workers) => workers.max(1),
}
}
}
pub(crate) struct Compressed {
pub data: Vec<u8>,
pub properties: Vec<u8>,
}
impl Compressed {
pub(crate) fn without_properties(data: Vec<u8>) -> Self {
Self {
data,
properties: Vec::new(),
}
}
}
#[cfg(feature = "lzma")]
pub(crate) fn dictionary_size(options: &WriteOptions, data_len: usize) -> u32 {
use crate::codec::lzma::{dict_size_covering, preset_dict_size};
preset_dict_size(options.level).min(dict_size_covering(data_len as u64))
}
#[cfg(feature = "lzma")]
pub(crate) fn stream_dictionary_size(options: &WriteOptions) -> u32 {
crate::codec::lzma::preset_dict_size(options.level)
}
#[cfg(feature = "parallel")]
pub(crate) fn encoder_memory_usage(
options: &WriteOptions,
#[cfg_attr(not(feature = "lzma"), allow(unused_variables))] data_len: usize,
) -> u64 {
#[allow(unused_imports)]
use crate::codec::CodecMethod;
const SMALL_ENCODER: u64 = 1 << 20;
#[allow(clippy::match_single_binding)]
match options.method {
#[cfg(feature = "lzma2")]
CodecMethod::Lzma2 => crate::codec::lzma::encoder_memory_usage(
options.level,
dictionary_size(options, data_len),
),
#[cfg(feature = "lzma")]
CodecMethod::Lzma => crate::codec::lzma::encoder_memory_usage(
options.level,
dictionary_size(options, data_len),
),
#[cfg(feature = "ppmd")]
CodecMethod::PPMd => {
let (_, mem_size) = ppmd_settings(options);
u64::from(mem_size) + SMALL_ENCODER
}
#[cfg(feature = "bzip2")]
CodecMethod::BZip2 => {
let block = u64::from(options.level.clamp(1, 9)) * 100 * 1024;
block * 8 + SMALL_ENCODER
}
_ => SMALL_ENCODER,
}
}
#[cfg(feature = "lzma2")]
pub(crate) fn lzma2_options(
options: &WriteOptions,
data_len: usize,
) -> crate::codec::lzma::Lzma2EncoderOptions {
crate::codec::lzma::Lzma2EncoderOptions {
preset: options.level,
dict_size: Some(dictionary_size(options, data_len)),
}
}
#[cfg(all(feature = "lzma2", feature = "parallel"))]
pub(crate) fn lzma2_is_chunked(
options: &WriteOptions,
encoder: &crate::codec::lzma::Lzma2EncoderOptions,
) -> bool {
!options.threads.is_single() && encoder.dict_size.is_some_and(|size| size > 0)
}
#[cfg(feature = "lzma2")]
pub(crate) fn compress_lzma2(
options: &WriteOptions,
data: &[u8],
#[cfg_attr(not(feature = "parallel"), allow(unused_variables))] concurrency: Concurrency,
) -> Result<Compressed> {
use crate::codec::lzma::Lzma2Encoder;
let opts = lzma2_options(options, data.len());
#[cfg(feature = "parallel")]
if concurrency.is_chunked(options, &opts, data.len()) {
use crate::codec::lzma2_chunked::ChunkedLzma2Encoder;
let mut output = Vec::new();
{
let mut encoder = ChunkedLzma2Encoder::new(
&mut output,
&opts,
concurrency.workers(),
options.memory_limit.bytes(),
)?;
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.finish().map_err(crate::Error::Io)?;
}
return Ok(Compressed {
data: output,
properties: opts.properties(),
});
}
let mut output = Vec::new();
{
let mut encoder = Lzma2Encoder::new(&mut output, &opts);
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.try_finish().map_err(crate::Error::Io)?;
}
Ok(Compressed {
data: output,
properties: opts.properties(),
})
}
#[cfg(feature = "lzma")]
pub(crate) fn compress_lzma(options: &WriteOptions, data: &[u8]) -> Result<Compressed> {
use crate::codec::lzma::{LzmaEncoder, LzmaEncoderOptions};
let opts = LzmaEncoderOptions {
preset: options.level,
dict_size: Some(dictionary_size(options, data.len())),
};
let mut output = Vec::new();
{
let mut encoder = LzmaEncoder::new(&mut output, &opts)?;
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.try_finish().map_err(crate::Error::Io)?;
}
Ok(Compressed {
data: output,
properties: opts.properties(),
})
}
#[cfg(feature = "deflate")]
pub(crate) fn compress_deflate(options: &WriteOptions, data: &[u8]) -> Result<Compressed> {
use crate::codec::deflate::{DeflateEncoder, DeflateEncoderOptions};
let opts = DeflateEncoderOptions {
level: options.level,
};
let mut output = Vec::new();
{
let mut encoder = DeflateEncoder::new(&mut output, &opts);
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.try_finish().map_err(crate::Error::Io)?;
}
Ok(Compressed::without_properties(output))
}
#[cfg(feature = "bzip2")]
pub(crate) fn compress_bzip2(options: &WriteOptions, data: &[u8]) -> Result<Compressed> {
use crate::codec::bzip2::{Bzip2Encoder, Bzip2EncoderOptions};
let opts = Bzip2EncoderOptions {
level: options.level,
};
let mut output = Vec::new();
{
let mut encoder = Bzip2Encoder::new(&mut output, &opts);
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.try_finish().map_err(crate::Error::Io)?;
}
Ok(Compressed::without_properties(output))
}
#[cfg(feature = "zstd")]
pub(crate) fn compress_zstd(options: &WriteOptions, data: &[u8]) -> Result<Compressed> {
use super::ZSTD_LEVEL_MAP;
use crate::codec::zstd::{ZstdEncoderOptions, ZstdStreamEncoder};
let zstd_level = ZSTD_LEVEL_MAP[options.level.min(9) as usize];
let opts = ZstdEncoderOptions { level: zstd_level };
let mut output = Vec::new();
{
let mut encoder = ZstdStreamEncoder::new(&mut output, &opts)
.map_err(|e| crate::Error::Io(std::io::Error::other(e)))?;
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.try_finish().map_err(crate::Error::Io)?;
}
Ok(Compressed::without_properties(output))
}
#[cfg(feature = "lz4")]
pub(crate) fn compress_lz4(_options: &WriteOptions, data: &[u8]) -> Result<Compressed> {
use crate::codec::lz4::{Lz4Encoder, Lz4EncoderOptions};
let opts = Lz4EncoderOptions::default();
let mut output = Vec::new();
{
let mut encoder = Lz4Encoder::new(&mut output, &opts);
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.try_finish().map_err(crate::Error::Io)?;
}
Ok(Compressed::without_properties(output))
}
#[cfg(feature = "brotli")]
pub(crate) fn compress_brotli(options: &WriteOptions, data: &[u8]) -> Result<Compressed> {
use super::BROTLI_QUALITY_MAP;
use crate::codec::brotli::{BrotliEncoder, BrotliEncoderOptions};
let quality = BROTLI_QUALITY_MAP[options.level.min(9) as usize];
let opts = BrotliEncoderOptions {
quality,
lg_window_size: 22,
};
let mut output = Vec::new();
{
let mut encoder = BrotliEncoder::new(&mut output, &opts);
encoder.write_all(data).map_err(crate::Error::Io)?;
encoder.try_finish().map_err(crate::Error::Io)?;
}
Ok(Compressed::without_properties(output))
}
#[cfg(feature = "ppmd")]
fn ppmd_settings(options: &WriteOptions) -> (u32, u32) {
match options.level {
0..=2 => (4, 4 * 1024 * 1024),
3..=4 => (6, 8 * 1024 * 1024),
5..=6 => (6, 16 * 1024 * 1024),
7..=8 => (8, 32 * 1024 * 1024),
_ => (8, 64 * 1024 * 1024),
}
}
#[cfg(feature = "ppmd")]
pub(crate) fn compress_ppmd(options: &WriteOptions, data: &[u8]) -> Result<Compressed> {
use crate::codec::Encoder;
use crate::codec::ppmd::{PpmdEncoder, PpmdEncoderOptions};
let (order, mem_size) = ppmd_settings(options);
let opts = PpmdEncoderOptions::new(order, mem_size);
let mut output = Vec::new();
{
let mut encoder = PpmdEncoder::new(&mut output, &opts)?;
encoder.write_all(data).map_err(crate::Error::Io)?;
Box::new(encoder).finish().map_err(crate::Error::Io)?;
}
let mut properties = vec![order as u8];
properties.extend_from_slice(&mem_size.to_le_bytes());
Ok(Compressed {
data: output,
properties,
})
}
#[cfg(all(test, feature = "lzma2", feature = "parallel"))]
mod tests {
use super::*;
use crate::Threads;
fn is_chunked(options: &WriteOptions) -> bool {
let encoder = lzma2_options(options, usize::MAX);
lzma2_is_chunked(options, &encoder)
}
#[test]
fn test_splitting_does_not_depend_on_the_machine() {
for threads in [2usize, 4, 16, 64] {
for limit in [64 << 20, 512 << 20, 8u64 << 30] {
let options = WriteOptions::new()
.level(5)
.expect("level")
.threads(Threads::count_or_single(threads))
.memory_limit(crate::MemoryLimit::bytes_or_auto(limit));
assert!(is_chunked(&options), "threads={threads} limit={limit}");
}
}
}
#[test]
fn test_a_single_thread_is_never_split() {
for threads in [Threads::Single, Threads::count_or_single(1)] {
let options = WriteOptions::new()
.level(5)
.expect("level")
.threads(threads);
assert!(!is_chunked(&options), "{threads:?} was split");
}
}
#[test]
fn test_the_split_threshold_matches_where_the_encoder_cuts() {
use crate::codec::lzma::Lzma2EncoderOptions;
use crate::codec::lzma2_chunked::{ChunkedLzma2Encoder, shortest_split_stream};
use std::io::Write;
let dictionary = 1u64 << 16;
let encoder_options = Lzma2EncoderOptions::with_preset(1).with_dict_size(dictionary as u32);
let boundary = shortest_split_stream(dictionary);
let unsplit = |len: usize| {
let mut out = Vec::new();
let mut encoder = crate::codec::lzma::Lzma2Encoder::new(&mut out, &encoder_options);
encoder.write_all(&vec![0u8; len]).expect("writes");
encoder.try_finish().expect("finishes");
out
};
let chunked = |len: usize| {
let mut out = Vec::new();
let mut encoder =
ChunkedLzma2Encoder::new(&mut out, &encoder_options, 4, u64::MAX).expect("builds");
encoder.write_all(&vec![0u8; len]).expect("writes");
encoder.finish().expect("finishes");
out
};
let options = WriteOptions::new()
.level(1)
.expect("level")
.threads(Threads::count_or_single(4));
for len in [boundary - 1, boundary, boundary + 1] {
let cut = chunked(len as usize) != unsplit(len as usize);
let predicted =
Concurrency::Alone(4).is_chunked(&options, &encoder_options, len as usize);
assert_eq!(
cut,
predicted,
"at {len} bytes the encoder {} but the writer says it {}",
if cut { "cut" } else { "did not cut" },
if predicted { "would" } else { "would not" },
);
}
}
#[test]
fn test_the_streaming_dictionary_matches_the_buffered_one() {
use super::super::streaming_entry::STREAMING_THRESHOLD;
for level in 0..=9u32 {
let options = WriteOptions::new().level(level).expect("level");
let streamed = stream_dictionary_size(&options);
assert!(
u64::from(streamed) <= STREAMING_THRESHOLD,
"level {level} wants a dictionary larger than the threshold, so the \
two paths would disagree on an entry just past it",
);
for size in [STREAMING_THRESHOLD + 1, STREAMING_THRESHOLD * 4, 4 << 30] {
assert_eq!(
dictionary_size(&options, size as usize),
streamed,
"level {level} at {size} bytes",
);
}
}
}
}