use std::io::{Seek, Write};
use crate::{ArchivePath, Result};
use super::compression::filter_and_compress_data;
use super::options::{EntryMeta, WriteOptions};
use super::{Bcj2FolderInfo, BufferedEntry, PendingEntry, Writer};
#[cfg(feature = "parallel")]
pub(crate) fn workers_within_budget(
options: &super::options::WriteOptions,
data_len: usize,
) -> usize {
let threads = options.threads.count();
let per_encoder = super::codecs::encoder_memory_usage(options, data_len);
let for_encoders = options
.memory_limit
.bytes()
.saturating_sub(batch_bytes(options));
for_encoders
.checked_div(per_encoder)
.and_then(|n| usize::try_from(n).ok())
.unwrap_or(threads)
.clamp(1, threads)
}
pub(crate) fn batch_bytes(options: &super::options::WriteOptions) -> u64 {
options.memory_limit.bytes() / 4
}
pub(crate) struct CompressedEntry {
compressed: super::codecs::Compressed,
filter_info: Option<super::FilteredFolderInfo>,
}
pub(crate) struct BatchOutcome {
path: ArchivePath,
meta: EntryMeta,
crc: u32,
uncompressed_size: u64,
compressed: Option<CompressedEntry>,
}
pub(crate) fn compress_batch_owned(
batch: Vec<BufferedEntry>,
options: &WriteOptions,
) -> Result<Vec<BatchOutcome>> {
let compress_one = |entry: BufferedEntry| -> Result<BatchOutcome> {
let BufferedEntry {
path,
data,
meta,
crc,
..
} = entry;
let uncompressed_size = data.len() as u64;
if data.is_empty() {
return Ok(BatchOutcome {
path,
meta,
crc,
uncompressed_size,
compressed: None,
});
}
let (compressed, filter_info) =
filter_and_compress_data(options, &data, super::codecs::Concurrency::Alongside)?;
drop(data);
Ok(BatchOutcome {
path,
meta,
crc,
uncompressed_size,
compressed: Some(CompressedEntry {
compressed,
filter_info,
}),
})
};
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
let largest = batch.iter().map(|e| e.data.len()).max().unwrap_or(0);
let workers = workers_within_budget(options, largest).min(batch.len());
if batch.len() > 1 && workers > 1 {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(workers)
.build()
.map_err(|e| crate::Error::Io(std::io::Error::other(e)))?;
return pool.install(|| batch.into_par_iter().map(compress_one).collect());
}
}
batch.into_iter().map(compress_one).collect()
}
impl<W: Write + Seek> Writer<W> {
pub(crate) fn compress_entry_non_solid(
&mut self,
archive_path: ArchivePath,
data: Vec<u8>,
meta: EntryMeta,
) -> Result<()> {
if self.options.filter.is_bcj2() {
self.flush_pending_batch()?;
return self.compress_entry_bcj2(archive_path, &data, meta);
}
let crc = crc32fast::hash(&data);
let options = self.active_options.clone();
self.pending_batch_size += data.len() as u64;
self.pending_batch.push(BufferedEntry {
options,
path: archive_path,
data,
meta,
crc,
});
if self.pending_batch_size >= batch_bytes(&self.options) || self.batch_can_fill_the_cores()
{
self.flush_pending_batch()?;
}
Ok(())
}
fn batch_can_fill_the_cores(&self) -> bool {
#[cfg(not(feature = "parallel"))]
{
true
}
#[cfg(feature = "parallel")]
{
let largest = self
.pending_batch
.iter()
.map(|e| e.data.len())
.max()
.unwrap_or(0);
self.pending_batch.len() >= workers_within_budget(&self.options, largest)
}
}
pub(crate) fn flush_pending_batch(&mut self) -> Result<()> {
if self.pending_batch.is_empty() {
return Ok(());
}
match self.flush_pending_batch_inner() {
Ok(()) => Ok(()),
Err(e) => self.fail(e),
}
}
fn flush_pending_batch_inner(&mut self) -> Result<()> {
let options = self
.pending_batch
.first()
.map(|entry| entry.options.clone())
.unwrap_or_else(|| self.active_options.clone());
let batch = std::mem::take(&mut self.pending_batch);
self.pending_batch_size = 0;
self.announce_entries(
batch
.iter()
.map(|entry| (entry.path.as_str().to_string(), entry.data.len() as u64))
.collect(),
);
let outcomes = compress_batch_owned(batch, &options)?;
self.write_batch_outcomes(outcomes, &options)
}
pub(crate) fn write_batch_outcomes(
&mut self,
outcomes: Vec<BatchOutcome>,
options: &WriteOptions,
) -> Result<()> {
for outcome in outcomes {
self.write_compressed_entry(outcome, options)?;
}
Ok(())
}
fn write_compressed_entry(
&mut self,
entry: BatchOutcome,
options: &WriteOptions,
) -> Result<()> {
let uncompressed_size = entry.uncompressed_size;
let pending = PendingEntry {
path: entry.path,
meta: entry.meta,
uncompressed_size,
};
let Some(CompressedEntry {
compressed,
filter_info,
}) = entry.compressed
else {
self.record_entry(pending);
return Ok(());
};
#[cfg(feature = "aes")]
let (output_data, encryption_info) = if options.is_data_encrypted() {
let (encrypted, enc_info) = self.encrypt_compressed_with(compressed, options)?;
(encrypted, Some(enc_info))
} else {
(compressed, None)
};
#[cfg(not(feature = "aes"))]
let (output_data, encryption_info) = (compressed, Option::<()>::None);
let packed_size = output_data.data.len() as u64;
self.write_entry_bytes(&output_data.data)?;
self.compressed_bytes += packed_size;
self.stream_info.pack_sizes.push(packed_size);
self.stream_info.unpack_sizes.push(uncompressed_size);
self.stream_info.coder_methods.push(options.method);
self.stream_info
.coder_properties
.push(output_data.properties);
self.stream_info.crcs.push(None);
self.stream_info.substream_sizes.push(uncompressed_size);
self.stream_info.substream_crcs.push(entry.crc);
#[cfg(feature = "aes")]
self.stream_info.encryption_info.push(encryption_info);
self.stream_info.filter_info.push(filter_info);
self.stream_info.bcj2_folder_info.push(None);
#[cfg(not(feature = "aes"))]
let _ = encryption_info;
self.stream_info.num_unpack_streams_per_folder.push(1);
self.record_entry(pending);
Ok(())
}
pub(crate) fn compress_entry_bcj2(
&mut self,
archive_path: ArchivePath,
data: &[u8],
meta: EntryMeta,
) -> Result<()> {
use crate::codec::bcj2::bcj2_encode;
let crc = crc32fast::hash(data);
let uncompressed_size = data.len() as u64;
let pending = PendingEntry {
path: archive_path,
meta,
uncompressed_size,
};
if data.is_empty() {
self.record_entry(pending);
return Ok(());
}
let streams = bcj2_encode(data);
let options = self.active_options.clone();
let concurrency = super::codecs::Concurrency::Alongside;
let main = super::compression::compress_data(&options, &streams.main, concurrency)?;
let call = super::compression::compress_data(&options, &streams.call, concurrency)?;
let jump = super::compression::compress_data(&options, &streams.jump, concurrency)?;
self.write_entry_bytes(&main.data)?;
self.write_entry_bytes(&call.data)?;
self.write_entry_bytes(&jump.data)?;
self.write_entry_bytes(&streams.range)?;
let total_packed =
(main.data.len() + call.data.len() + jump.data.len() + streams.range.len()) as u64;
self.compressed_bytes += total_packed;
let bcj2_info = Bcj2FolderInfo {
pack_sizes: [
main.data.len() as u64,
call.data.len() as u64,
jump.data.len() as u64,
streams.range.len() as u64,
],
stream_sizes: [
streams.main.len() as u64,
streams.call.len() as u64,
streams.jump.len() as u64,
],
properties: [main.properties, call.properties, jump.properties],
method: options.method,
};
self.stream_info.unpack_sizes.push(uncompressed_size);
self.stream_info.coder_methods.push(self.options.method);
self.stream_info.coder_properties.push(Vec::new());
self.stream_info.crcs.push(None);
self.stream_info.substream_sizes.push(uncompressed_size);
self.stream_info.substream_crcs.push(crc);
self.stream_info.filter_info.push(None);
self.stream_info.bcj2_folder_info.push(Some(bcj2_info));
#[cfg(feature = "aes")]
self.stream_info.encryption_info.push(None);
self.stream_info.num_unpack_streams_per_folder.push(1);
self.record_entry(pending);
Ok(())
}
pub(crate) fn buffer_entry_solid(
&mut self,
archive_path: ArchivePath,
data: Vec<u8>,
meta: EntryMeta,
) -> Result<()> {
let crc = crc32fast::hash(&data);
let data_size = data.len() as u64;
self.solid_buffer_size += data_size;
let options = self.active_options.clone();
self.solid_buffer.push(BufferedEntry {
options,
path: archive_path,
data,
meta,
crc,
});
let size_exceeded = self
.options
.solid
.block_size
.is_some_and(|limit| self.solid_buffer_size >= limit);
let count_exceeded = self
.options
.solid
.files_per_block
.is_some_and(|limit| self.solid_buffer.len() >= limit);
if size_exceeded || count_exceeded {
self.flush_solid_buffer()?;
}
Ok(())
}
pub(crate) fn flush_solid_buffer(&mut self) -> Result<()> {
if self.solid_buffer.is_empty() {
return Ok(());
}
match self.flush_solid_buffer_inner() {
Ok(()) => Ok(()),
Err(e) => self.fail(e),
}
}
fn flush_solid_buffer_inner(&mut self) -> Result<()> {
let options = self
.solid_buffer
.first()
.map(|entry| entry.options.clone())
.unwrap_or_else(|| self.active_options.clone());
self.announce_entries(
self.solid_buffer
.iter()
.map(|entry| (entry.path.as_str().to_string(), entry.data.len() as u64))
.collect(),
);
let total_uncompressed: u64 = self.solid_buffer.iter().map(|e| e.data.len() as u64).sum();
let mut combined = Vec::with_capacity(total_uncompressed as usize);
let mut sizes = Vec::new();
let mut crcs = Vec::new();
let mut num_streams = 0u64;
for entry in &self.solid_buffer {
if !entry.data.is_empty() {
combined.extend_from_slice(&entry.data);
sizes.push(entry.data.len() as u64);
crcs.push(entry.crc);
num_streams += 1;
}
}
if num_streams == 0 {
for entry in self.solid_buffer.drain(..).collect::<Vec<_>>() {
self.record_entry(PendingEntry {
path: entry.path,
meta: entry.meta,
uncompressed_size: 0,
});
}
return Ok(());
}
let concurrency = super::codecs::Concurrency::alone(&options, combined.len());
let (compressed, filter_info) = filter_and_compress_data(&options, &combined, concurrency)?;
#[cfg(feature = "aes")]
let (output_data, encryption_info) = if options.is_data_encrypted() {
let (encrypted, enc_info) = self.encrypt_compressed_with(compressed, &options)?;
(encrypted, Some(enc_info))
} else {
(compressed, None)
};
#[cfg(not(feature = "aes"))]
let (output_data, encryption_info) = (compressed, Option::<()>::None);
let packed_size = output_data.data.len() as u64;
self.write_entry_bytes(&output_data.data)?;
self.compressed_bytes += packed_size;
self.stream_info.pack_sizes.push(packed_size);
self.stream_info.unpack_sizes.push(total_uncompressed);
self.stream_info.coder_methods.push(options.method);
self.stream_info
.coder_properties
.push(output_data.properties);
#[cfg(feature = "aes")]
self.stream_info.encryption_info.push(encryption_info);
self.stream_info.filter_info.push(filter_info);
self.stream_info.bcj2_folder_info.push(None);
#[cfg(not(feature = "aes"))]
let _ = encryption_info;
self.stream_info
.num_unpack_streams_per_folder
.push(num_streams);
if num_streams == 1 {
self.stream_info.crcs.push(None);
self.stream_info.substream_sizes.extend_from_slice(&sizes);
self.stream_info.substream_crcs.extend_from_slice(&crcs);
} else {
self.stream_info.crcs.push(None);
self.stream_info.substream_sizes.extend_from_slice(&sizes);
self.stream_info.substream_crcs.extend_from_slice(&crcs);
}
for entry in self.solid_buffer.drain(..).collect::<Vec<_>>() {
let uncompressed_size = entry.data.len() as u64;
self.record_entry(PendingEntry {
path: entry.path,
meta: entry.meta,
uncompressed_size,
});
}
self.solid_buffer_size = 0;
Ok(())
}
}