#![warn(missing_docs)]
#![cfg_attr(feature = "wasi_fs", feature(wasi_ext))]
use std::{
borrow::Cow,
io::{Read, Seek, Write},
num::NonZeroUsize,
panic::{RefUnwindSafe, UnwindSafe},
path::Path,
sync::{mpsc, Mutex},
};
use level::CompressionLevel;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use zip_archive_parts::{
data::ZipData,
extra_field::{ExtraField, ExtraFields},
file::ZipFile,
job::{ZipJob, ZipJobOrigin},
};
pub mod level;
mod platform;
mod zip_archive_parts;
pub use zip_archive_parts::extra_field;
#[repr(u16)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CompressionType {
Stored = 0,
#[default]
Deflate = 8,
}
#[must_use]
#[derive(Debug)]
pub struct ZipFileBuilder<'a, 'd, 'p, 'r> {
archive_handle: &'a mut ZipArchive<'d, 'p, 'r>,
job: ZipJob<'d, 'p, 'r>,
}
impl<'a, 'd, 'p, 'r> ZipFileBuilder<'a, 'd, 'p, 'r> {
pub fn done(self) {
let Self {
archive_handle,
job,
} = self;
match &job.data_origin {
ZipJobOrigin::Directory => {
let file = job.into_file().expect("No failing code path");
archive_handle.push_file(file);
}
_ => archive_handle.push_job(job),
}
}
pub fn metadata_from_fs(self, fs_path: &Path) -> std::io::Result<Self> {
let metadata = std::fs::metadata(fs_path)?;
let external_attributes = platform::attributes_from_fs(&metadata);
let extra_fields = ExtraFields::new_from_fs(&metadata);
Ok(self
.external_attributes(external_attributes)
.extra_fields(extra_fields))
}
pub fn file_comment(mut self, comment: String) -> Self {
self.job.file_comment = Some(comment);
self
}
pub fn extra_field(mut self, extra_field: ExtraField) -> Self {
self.job.extra_fields.values.push(extra_field);
self
}
pub fn extra_fields(mut self, extra_fields: impl IntoIterator<Item = ExtraField>) -> Self {
self.job.extra_fields.extend(extra_fields);
self
}
pub fn compression_type(mut self, compression_type: CompressionType) -> Self {
self.job.compression_type = compression_type;
self
}
pub fn compression_level(mut self, compression_level: CompressionLevel) -> Self {
self.job.compression_level = compression_level;
self
}
pub fn external_attributes(mut self, external_attributes: u16) -> Self {
self.job.external_attributes = external_attributes;
self
}
pub fn external_attributes_from_fs(mut self, fs_path: &Path) -> std::io::Result<Self> {
let metadata = std::fs::metadata(fs_path)?;
self.job.external_attributes = platform::attributes_from_fs(&metadata);
Ok(self)
}
#[inline]
fn new(
archive: &'a mut ZipArchive<'d, 'p, 'r>,
filename: String,
origin: ZipJobOrigin<'d, 'p, 'r>,
) -> Self {
Self {
archive_handle: archive,
job: ZipJob {
data_origin: origin,
archive_path: filename,
extra_fields: ExtraFields::default(),
file_comment: None,
external_attributes: platform::default_file_attrs(),
compression_type: CompressionType::Deflate,
compression_level: CompressionLevel::best(),
},
}
}
#[inline]
fn new_dir(archive: &'a mut ZipArchive<'d, 'p, 'r>, filename: String) -> Self {
Self {
archive_handle: archive,
job: ZipJob {
data_origin: ZipJobOrigin::Directory,
archive_path: filename,
extra_fields: ExtraFields::default(),
file_comment: None,
external_attributes: platform::default_dir_attrs(),
compression_type: CompressionType::Deflate,
compression_level: CompressionLevel::best(),
},
}
}
}
#[derive(Debug, Default)]
pub struct ZipArchive<'d, 'p, 'r> {
jobs_queue: Vec<ZipJob<'d, 'p, 'r>>,
data: ZipData,
}
impl<'d, 'p, 'r> ZipArchive<'d, 'p, 'r> {
fn push_job(&mut self, job: ZipJob<'d, 'p, 'r>) {
self.jobs_queue.push(job);
}
fn push_file(&mut self, file: ZipFile) {
self.data.files.push(file);
}
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn add_file_from_fs(
&mut self,
fs_path: impl Into<Cow<'p, Path>>,
archived_path: String,
) -> ZipFileBuilder<'_, 'd, 'p, 'r> {
ZipFileBuilder::new(
self,
archived_path,
ZipJobOrigin::Filesystem {
path: fs_path.into(),
},
)
}
#[inline]
pub fn add_file_from_memory(
&mut self,
data: impl Into<Cow<'d, [u8]>>,
archived_path: String,
) -> ZipFileBuilder<'_, 'd, 'p, 'r> {
ZipFileBuilder::new(self, archived_path, ZipJobOrigin::RawData(data.into()))
}
#[inline]
pub fn add_file_from_reader<R: Read + Send + Sync + UnwindSafe + RefUnwindSafe + 'r>(
&mut self,
reader: R,
archived_path: String,
) -> ZipFileBuilder<'_, 'd, 'p, 'r> {
ZipFileBuilder::new(self, archived_path, ZipJobOrigin::Reader(Box::new(reader)))
}
#[inline]
pub fn add_directory(&mut self, archived_path: String) -> ZipFileBuilder<'_, 'd, 'p, 'r> {
ZipFileBuilder::new_dir(self, archived_path)
}
#[inline]
pub fn compress(&mut self) {
self.compress_with_threads(Self::get_threads());
}
#[inline]
pub fn compress_with_threads(&mut self, threads: usize) {
if !self.jobs_queue.is_empty() {
self.compress_with_consumer(threads, |zip_data, rx| zip_data.files.extend(rx))
}
}
#[inline]
pub fn write<W: Write + Seek>(&mut self, writer: &mut W) -> std::io::Result<()> {
self.write_with_threads(writer, Self::get_threads())
}
#[inline]
pub fn write_with_threads<W: Write + Seek>(
&mut self,
writer: &mut W,
threads: usize,
) -> std::io::Result<()> {
if !self.jobs_queue.is_empty() {
self.compress_with_consumer(threads, |zip_data, rx| zip_data.write(writer, rx))
} else {
self.data.write(writer, std::iter::empty())
}
}
fn compress_with_consumer<F, T>(&mut self, threads: usize, consumer: F) -> T
where
F: FnOnce(&mut ZipData, mpsc::Receiver<ZipFile>) -> T,
{
let jobs_drain = Mutex::new(self.jobs_queue.drain(..));
let jobs_drain_ref = &jobs_drain;
std::thread::scope(|s| {
let rx = {
let (tx, rx) = mpsc::channel();
for _ in 0..threads {
let thread_tx = tx.clone();
s.spawn(move || loop {
let next_job = jobs_drain_ref.lock().unwrap().next_back();
if let Some(job) = next_job {
thread_tx.send(job.into_file().unwrap()).unwrap();
} else {
break;
}
});
}
rx
};
consumer(&mut self.data, rx)
})
}
fn get_threads() -> usize {
std::thread::available_parallelism()
.map(NonZeroUsize::get)
.unwrap_or(1)
}
}
#[cfg(feature = "rayon")]
impl<'d, 'p, 'r> ZipArchive<'d, 'p, 'r> {
pub fn compress_with_rayon(&mut self) {
if !self.jobs_queue.is_empty() {
let files_par_iter = self
.jobs_queue
.par_drain(..)
.map(|job| job.into_file().unwrap());
self.data.files.par_extend(files_par_iter)
}
}
pub fn write_with_rayon<W: Write + Seek + Send>(
&mut self,
writer: &mut W,
) -> std::io::Result<()> {
if !self.jobs_queue.is_empty() {
let files_par_iter = self
.jobs_queue
.par_drain(..)
.map(|job| job.into_file().unwrap());
self.data.write_rayon(writer, files_par_iter)
} else {
self.data.write_rayon(writer, rayon::iter::empty())
}
}
}