use std::io::{Read, Seek, Write};
use crate::codec::Encoder;
use crate::{ArchivePath, Error, Result};
use super::options::{EntryMeta, WriteOptions};
use super::{PendingEntry, Writer};
pub(crate) const STREAMING_THRESHOLD: u64 = 64 * 1024 * 1024;
const READ_CHUNK: usize = 256 * 1024;
struct InFlightBatch {
handle: std::thread::JoinHandle<Result<Vec<super::entry_compression::BatchOutcome>>>,
options: WriteOptions,
footprint: u64,
}
#[cfg(feature = "parallel")]
fn batch_footprint(batch: &[super::BufferedEntry], options: &WriteOptions) -> u64 {
let entries: u64 = batch.iter().map(|entry| entry.data.len() as u64).sum();
let largest = batch
.iter()
.map(|entry| entry.data.len())
.max()
.unwrap_or(0);
let workers = super::entry_compression::workers_within_budget(options, largest)
.min(batch.len())
.max(1) as u64;
entries.saturating_mul(4).saturating_add(
workers.saturating_mul(super::codecs::encoder_memory_usage(options, largest)),
)
}
#[derive(Clone)]
struct HoldingArea {
held: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
}
impl HoldingArea {
fn new() -> Self {
Self {
held: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
fn waiting(&self) -> usize {
self.held.lock().map_or(0, |held| held.len())
}
fn swap_into(&self, taker: &mut Vec<u8>) -> Result<()> {
let mut held = self
.held
.lock()
.map_err(|_| Error::Io(std::io::Error::other("compression thread failed")))?;
std::mem::swap(&mut *held, taker);
Ok(())
}
}
impl Write for HoldingArea {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut held = self
.held
.lock()
.map_err(|_| std::io::Error::other("compression thread failed"))?;
held.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn time_to_collect(batch_finished: bool, waiting: usize, cap: usize) -> bool {
batch_finished || waiting >= cap
}
pub(crate) struct StreamedFolder {
path: ArchivePath,
meta: EntryMeta,
uncompressed_size: u64,
crc: u32,
packed_size: u64,
properties: Vec<u8>,
method: crate::codec::CodecMethod,
}
struct StreamedEntry {
crc: crc32fast::Hasher,
uncompressed_size: u64,
}
struct CountingWriter<W> {
inner: W,
written: std::sync::Arc<std::sync::atomic::AtomicU64>,
watcher: Option<Watcher>,
}
struct PumpSetup<'a> {
options: &'a WriteOptions,
reserved: u64,
watcher: Option<Watcher>,
}
struct Watcher {
reporter: std::sync::Arc<std::sync::Mutex<Box<dyn crate::progress::ProgressReporter>>>,
declared: u64,
called_off: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl<W: Write> CountingWriter<W> {
fn new(inner: W) -> Self {
Self {
inner,
written: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
watcher: None,
}
}
fn watched(mut self, watcher: Option<Watcher>) -> Self {
self.watcher = watcher;
self
}
}
impl<W: Write> Write for CountingWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let n = self.inner.write(buf)?;
let written = self
.written
.fetch_add(n as u64, std::sync::atomic::Ordering::Relaxed)
+ n as u64;
let mut called_off = false;
if let Some(watcher) = self.watcher.as_ref() {
if let Ok(mut held) = watcher.reporter.lock() {
called_off = !held.on_progress(written, watcher.declared);
}
}
if called_off {
if let Some(watcher) = self.watcher.as_ref() {
watcher
.called_off
.store(true, std::sync::atomic::Ordering::Relaxed);
}
return Err(std::io::Error::other(
"the progress reporter called the write off",
));
}
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
pub(super) fn read_some(source: &mut dyn Read, buffer: &mut [u8]) -> Result<usize> {
let mut filled = 0;
while filled < buffer.len() {
let read = source.read(&mut buffer[filled..]).map_err(Error::Io)?;
if read == 0 {
break;
}
filled += read;
}
Ok(filled)
}
pub(crate) fn can_stream(options: &WriteOptions) -> bool {
if options.solid.is_solid() || options.filter.is_active() {
return false;
}
#[cfg(feature = "aes")]
if options.is_data_encrypted() {
return false;
}
encoder_is_available(options)
}
fn encoder_is_available(options: &WriteOptions) -> bool {
use crate::codec::CodecMethod;
match options.method {
CodecMethod::Copy => true,
#[cfg(feature = "lzma2")]
CodecMethod::Lzma2 => true,
#[cfg(feature = "lzma")]
CodecMethod::Lzma => true,
_ => false,
}
}
struct StoreEncoder<W> {
inner: W,
}
impl<W: Write + Send> Write for StoreEncoder<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
impl<W: Write + Send> Encoder for StoreEncoder<W> {
fn method_id(&self) -> &'static [u8] {
crate::codec::method::COPY
}
fn finish(mut self: Box<Self>) -> std::io::Result<()> {
self.inner.flush()
}
}
fn overlap_share(options: &WriteOptions) -> usize {
usize::try_from(options.memory_limit.bytes() / 8).unwrap_or(usize::MAX)
}
fn overlap_reservation(batch_footprint: u64, options: &WriteOptions) -> u64 {
batch_footprint.saturating_add(overlap_share(options) as u64)
}
fn holding_cap(options: &WriteOptions) -> usize {
overlap_share(options) / 2
}
fn encoder_for<'a, W: Write + Send + 'a>(
options: &WriteOptions,
output: W,
reserved: u64,
) -> Result<(Box<dyn Encoder + 'a>, Vec<u8>)> {
use crate::codec::CodecMethod;
#[cfg(not(all(feature = "parallel", feature = "lzma2")))]
let _ = reserved;
match options.method {
CodecMethod::Copy => Ok((Box::new(StoreEncoder { inner: output }), Vec::new())),
#[cfg(feature = "lzma2")]
CodecMethod::Lzma2 => {
use crate::codec::lzma::{Lzma2Encoder, Lzma2EncoderOptions};
let opts = Lzma2EncoderOptions {
preset: options.level,
dict_size: Some(super::codecs::stream_dictionary_size(options)),
};
let properties = opts.properties();
#[cfg(feature = "parallel")]
if super::codecs::lzma2_is_chunked(options, &opts) {
use crate::codec::lzma2_chunked::ChunkedLzma2Encoder;
let encoder = ChunkedLzma2Encoder::new(
output,
&opts,
options.threads.count(),
options.memory_limit.bytes().saturating_sub(reserved),
)?;
return Ok((Box::new(encoder), properties));
}
Ok((Box::new(Lzma2Encoder::new(output, &opts)), properties))
}
#[cfg(feature = "lzma")]
CodecMethod::Lzma => {
use crate::codec::lzma::{LzmaEncoder, LzmaEncoderOptions};
let opts = LzmaEncoderOptions {
preset: options.level,
dict_size: Some(super::codecs::stream_dictionary_size(options)),
};
let properties = opts.properties();
Ok((Box::new(LzmaEncoder::new(output, &opts)?), properties))
}
method => Err(Error::UnsupportedMethod {
method_id: method.method_id(),
}),
}
}
impl<W: Write + Seek + Send> Writer<W> {
fn send_batch_ahead(&mut self) -> Result<Option<InFlightBatch>> {
#[cfg(not(feature = "parallel"))]
return Ok(None);
#[cfg(feature = "parallel")]
{
if self.pending_batch.is_empty() {
return Ok(None);
}
if !self.solid_buffer.is_empty() {
return Ok(None);
}
let options = self
.pending_batch
.first()
.map(|entry| (*entry.options).clone())
.unwrap_or_else(|| (*self.active_options).clone());
if options.threads.count() <= 1 {
return Ok(None);
}
let batch = std::mem::take(&mut self.pending_batch);
self.pending_batch_size = 0;
let footprint = batch_footprint(&batch, &options);
if footprint > overlap_share(&options) as u64 {
self.pending_batch = batch;
self.pending_batch_size =
self.pending_batch.iter().map(|e| e.data.len() as u64).sum();
return Ok(None);
}
self.announce_entries(
batch
.iter()
.map(|entry| (entry.path.as_str().to_string(), entry.data.len() as u64))
.collect(),
);
let for_thread = options.clone();
let handle = match std::thread::Builder::new()
.name("zesven-batch".into())
.spawn(move || super::entry_compression::compress_batch_owned(batch, &for_thread))
{
Ok(handle) => handle,
Err(e) => return self.fail(Error::Io(e)),
};
Ok(Some(InFlightBatch {
handle,
options,
footprint,
}))
}
}
fn collect_batch(&mut self, in_flight: &mut Option<InFlightBatch>) -> Result<()> {
let Some(InFlightBatch {
handle, options, ..
}) = in_flight.take()
else {
return Ok(());
};
let outcomes = handle
.join()
.map_err(|_| Error::Io(std::io::Error::other("compressing a batch panicked")))??;
self.write_batch_outcomes(outcomes, &options)
}
fn abandon_batch(&mut self, in_flight: &mut Option<InFlightBatch>) {
if let Some(InFlightBatch { handle, .. }) = in_flight.take() {
drop(handle.join());
}
}
fn pump<O: Write + Send>(
source: &mut dyn Read,
prefix: Vec<u8>,
output: O,
setup: PumpSetup<'_>,
state: &mut StreamedEntry,
mut between: impl FnMut(u64) -> Result<()>,
) -> Result<(u64, Vec<u8>)> {
let PumpSetup {
options,
reserved,
watcher,
} = setup;
let mut buffer = vec![0u8; READ_CHUNK];
let mut counting = CountingWriter::new(output).watched(watcher);
let produced = std::sync::Arc::clone(&counting.written);
let (mut encoder, properties) = encoder_for(options, &mut counting, reserved)?;
state.crc.update(&prefix);
state.uncompressed_size += prefix.len() as u64;
let mut result = Ok(());
for piece in prefix.chunks(READ_CHUNK) {
if let Err(e) = encoder.write_all(piece) {
result = Err(Error::Io(e));
break;
}
if let Err(e) = between(produced.load(std::sync::atomic::Ordering::Relaxed)) {
result = Err(e);
break;
}
}
drop(prefix);
while result.is_ok() {
let read = match read_some(source, &mut buffer) {
Ok(n) => n,
Err(e) => {
result = Err(e);
break;
}
};
if read == 0 {
break;
}
state.crc.update(&buffer[..read]);
state.uncompressed_size += read as u64;
if let Err(e) = encoder.write_all(&buffer[..read]) {
result = Err(Error::Io(e));
break;
}
if let Err(e) = between(produced.load(std::sync::atomic::Ordering::Relaxed)) {
result = Err(e);
break;
}
}
let finished = encoder.finish().map_err(Error::Io);
result.and(finished)?;
Ok((
counting.written.load(std::sync::atomic::Ordering::Relaxed),
properties,
))
}
pub(crate) fn record_streamed_folder(&mut self, folder: StreamedFolder) {
let StreamedFolder {
path,
meta,
uncompressed_size,
crc,
packed_size,
properties,
method,
} = folder;
let pending = PendingEntry {
path,
meta,
uncompressed_size,
};
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(method);
self.stream_info.coder_properties.push(properties);
self.stream_info.crcs.push(None);
self.stream_info.substream_sizes.push(uncompressed_size);
self.stream_info.substream_crcs.push(crc);
#[cfg(feature = "aes")]
self.stream_info.encryption_info.push(None);
self.stream_info.filter_info.push(None);
self.stream_info.bcj2_folder_info.push(None);
self.stream_info.num_unpack_streams_per_folder.push(1);
self.record_entry(pending);
}
fn pour(&mut self, holding: &HoldingArea, scratch: &mut Vec<u8>) -> Result<()> {
scratch.clear();
holding.swap_into(scratch)?;
if scratch.is_empty() {
return Ok(());
}
self.sink.write_all(scratch).map_err(Error::Io)
}
pub(crate) fn compress_entry_streaming(
&mut self,
archive_path: ArchivePath,
prefix: Vec<u8>,
source: &mut dyn Read,
meta: EntryMeta,
) -> Result<()> {
let mut in_flight = self.send_batch_ahead()?;
if let Err(e) = self.flush_buffered_entries() {
self.abandon_batch(&mut in_flight);
return self.fail(e);
}
let mut state = StreamedEntry {
crc: crc32fast::Hasher::new(),
uncompressed_size: 0,
};
let holding = HoldingArea::new();
let mut scratch = Vec::new();
let held_limit = holding_cap(&self.options);
let options = self.options.clone();
let declared = meta.size;
self.announce_entries(vec![(archive_path.as_str().to_string(), declared)]);
let shared = self.progress.clone();
let called_off = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let watcher = || {
shared.as_ref().map(|reporter| Watcher {
reporter: std::sync::Arc::clone(reporter),
declared,
called_off: std::sync::Arc::clone(&called_off),
})
};
let outcome = match in_flight.as_ref() {
None => Self::pump(
source,
prefix,
&mut self.sink,
PumpSetup {
options: &options,
reserved: 0,
watcher: watcher(),
},
&mut state,
|_| Ok(()),
),
Some(batch) => {
let reserved = overlap_reservation(batch.footprint, &options);
let output = holding.clone();
Self::pump(
source,
prefix,
output,
PumpSetup {
options: &options,
reserved,
watcher: watcher(),
},
&mut state,
|_| {
if in_flight.as_ref().is_some_and(|batch| {
time_to_collect(
batch.handle.is_finished(),
holding.waiting(),
held_limit,
)
}) {
self.collect_batch(&mut in_flight)?;
}
if in_flight.is_none() {
self.pour(&holding, &mut scratch)?;
}
Ok(())
},
)
}
};
drop(shared);
let (packed_size, properties) = match outcome {
Ok(values) => values,
Err(e) => {
self.abandon_batch(&mut in_flight);
let e = if called_off.load(std::sync::atomic::Ordering::Relaxed) {
Error::Cancelled
} else {
e
};
return self.fail(e);
}
};
let settled = self
.collect_batch(&mut in_flight)
.and_then(|()| self.pour(&holding, &mut scratch));
if let Err(e) = settled {
self.abandon_batch(&mut in_flight);
return self.fail(e);
}
let StreamedEntry {
crc,
uncompressed_size,
} = state;
self.record_streamed_folder(StreamedFolder {
path: archive_path,
meta,
uncompressed_size,
crc: crc.finalize(),
packed_size,
properties,
method: self.options.method,
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::time_to_collect;
#[test]
fn test_the_window_taken_covers_both_sides_of_the_overlap() {
use super::{overlap_reservation, overlap_share};
use crate::write::options::WriteOptions;
let options = WriteOptions::new();
let footprint = 40 << 20;
let share = overlap_share(&options) as u64;
assert_eq!(super::holding_cap(&options) as u64 * 2, share);
assert_eq!(overlap_reservation(footprint, &options), footprint + share);
}
#[cfg(feature = "parallel")]
#[test]
fn test_a_batch_is_charged_for_its_output_as_well_as_its_input() {
use super::super::BufferedEntry;
use super::batch_footprint;
use crate::ArchivePath;
use crate::write::options::{EntryMeta, WriteOptions};
let options = std::sync::Arc::new(WriteOptions::new());
let entry = |name: &str, len: usize| BufferedEntry {
path: ArchivePath::new(name).expect("path"),
data: vec![0u8; len],
meta: EntryMeta::file(len as u64),
crc: 0,
options: options.clone(),
};
let lean = vec![entry("a.bin", 16 << 20), entry("b.bin", 1)];
let full = vec![entry("a.bin", 16 << 20), entry("b.bin", 8 << 20)];
let more = ((8 << 20) - 1) as u64;
let grown = batch_footprint(&full, &options) - batch_footprint(&lean, &options);
assert_eq!(
grown,
more * 4,
"{more} more bytes in a batch grew what it is charged by {grown}: \
its output, or the room the vector holding that output reserves \
past it, is not being charged beside its input",
);
}
#[test]
fn test_a_batch_is_collected_when_it_ends_or_when_there_is_no_room() {
assert!(!time_to_collect(false, 0, 64));
assert!(!time_to_collect(false, 63, 64));
assert!(time_to_collect(true, 0, 64));
assert!(time_to_collect(false, 64, 64));
assert!(time_to_collect(false, 65, 64));
}
}