Merge pull request #286 from barsgroup/add-compression-level

Add support for specifying compression level
This commit is contained in:
Alexander Zaitsev 2022-03-25 11:49:44 +03:00 committed by GitHub
commit cdcef7db09
Signed by: DevComp
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 104 additions and 16 deletions

View file

@ -500,9 +500,9 @@ impl<R: Read + io::Seek> ZipArchive<R> {
}
/// Search for a file entry by name, decrypt with given password
///
///
/// # Warning
///
///
/// The implementation of the cryptographic algorithms has not
/// gone through a correctness review, and you should assume it is insecure:
/// passwords used with this API may be compromised.
@ -534,9 +534,9 @@ impl<R: Read + io::Seek> ZipArchive<R> {
}
/// Get a contained file by index, decrypt with given password
///
///
/// # Warning
///
///
/// The implementation of the cryptographic algorithms has not
/// gone through a correctness review, and you should assume it is insecure:
/// passwords used with this API may be compromised.
@ -679,6 +679,7 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>(
#[allow(deprecated)]
CompressionMethod::from_u16(compression_method)
},
compression_level: None,
last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time),
crc32,
compressed_size: compressed_size as u64,
@ -1074,6 +1075,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>(
encrypted,
using_data_descriptor,
compression_method,
compression_level: None,
last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time),
crc32,
compressed_size: compressed_size as u64,

View file

@ -265,6 +265,8 @@ pub struct ZipFileData {
pub using_data_descriptor: bool,
/// Compression method used to store the file
pub compression_method: crate::compression::CompressionMethod,
/// Compression level to store the file
pub compression_level: Option<i32>,
/// Last modified time. This will only have a 2 second precision.
pub last_modified_time: DateTime,
/// CRC32 checksum
@ -396,6 +398,7 @@ mod test {
encrypted: false,
using_data_descriptor: false,
compression_method: crate::compression::CompressionMethod::Stored,
compression_level: None,
last_modified_time: DateTime::default(),
crc32: 0,
compressed_size: 0,

View file

@ -11,6 +11,7 @@ use std::default::Default;
use std::io;
use std::io::prelude::*;
use std::mem;
use std::ops::RangeInclusive;
#[cfg(any(
feature = "deflate",
@ -103,6 +104,7 @@ struct ZipRawValues {
#[derive(Copy, Clone)]
pub struct FileOptions {
compression_method: CompressionMethod,
compression_level: Option<i32>,
last_modified_time: DateTime,
permissions: Option<u32>,
large_file: bool,
@ -124,6 +126,7 @@ impl FileOptions {
feature = "deflate-zlib"
)))]
compression_method: CompressionMethod::Stored,
compression_level: None,
#[cfg(feature = "time")]
last_modified_time: DateTime::from_time(OffsetDateTime::now_utc()).unwrap_or_default(),
#[cfg(not(feature = "time"))]
@ -143,6 +146,21 @@ impl FileOptions {
self
}
/// Set the compression level for the new file
///
/// `None` value specifies default compression level.
///
/// Range of values depends on compression method:
/// * `Deflated`: 0 - 9. Default is 6
/// * `Bzip2`: 0 - 9. Default is 6
/// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3
/// * others: only `None` is allowed
#[must_use]
pub fn compression_level(mut self, level: Option<i32>) -> FileOptions {
self.compression_level = level;
self
}
/// Set the last modified time
///
/// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01
@ -340,6 +358,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
encrypted: false,
using_data_descriptor: false,
compression_method: options.compression_method,
compression_level: options.compression_level,
last_modified_time: options.last_modified_time,
crc32: raw_values.crc32,
compressed_size: raw_values.compressed_size,
@ -375,7 +394,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
// Implicitly calling [`ZipWriter::end_extra_data`] for empty files.
self.end_extra_data()?;
}
self.inner.switch_to(CompressionMethod::Stored)?;
self.inner.switch_to(CompressionMethod::Stored, None)?;
let writer = self.inner.get_plain();
if !self.writing_raw {
@ -410,7 +429,8 @@ impl<W: Write + io::Seek> ZipWriter<W> {
}
*options.permissions.as_mut().unwrap() |= 0o100000;
self.start_entry(name, options, None)?;
self.inner.switch_to(options.compression_method)?;
self.inner
.switch_to(options.compression_method, options.compression_level)?;
self.writing_to_file = true;
Ok(())
}
@ -587,7 +607,8 @@ impl<W: Write + io::Seek> ZipWriter<W> {
writer.write_u16::<LittleEndian>(extra_field_length)?;
writer.seek(io::SeekFrom::Start(header_end))?;
self.inner.switch_to(file.compression_method)?;
self.inner
.switch_to(file.compression_method, file.compression_level)?;
}
self.writing_to_extra_field = false;
@ -803,7 +824,11 @@ impl<W: Write + io::Seek> Drop for ZipWriter<W> {
}
impl<W: Write + io::Seek> GenericZipWriter<W> {
fn switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()> {
fn switch_to(
&mut self,
compression: CompressionMethod,
compression_level: Option<i32>,
) -> ZipResult<()> {
match self.current_compression() {
Some(method) if method == compression => return Ok(()),
None => {
@ -840,7 +865,15 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
*self = {
#[allow(deprecated)]
match compression {
CompressionMethod::Stored => GenericZipWriter::Storer(bare),
CompressionMethod::Stored => {
if let Some(_) = compression_level {
return Err(ZipError::UnsupportedArchive(
"Unsupported compression level",
));
}
GenericZipWriter::Storer(bare)
}
#[cfg(any(
feature = "deflate",
feature = "deflate-miniz",
@ -848,21 +881,50 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
))]
CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
bare,
flate2::Compression::default(),
flate2::Compression::new(
clamp_opt(
compression_level
.unwrap_or(flate2::Compression::default().level() as i32),
deflate_compression_level_range(),
)
.ok_or(ZipError::UnsupportedArchive(
"Unsupported compression level",
))? as u32,
),
)),
#[cfg(feature = "bzip2")]
CompressionMethod::Bzip2 => {
GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::default()))
}
CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new(
bare,
bzip2::Compression::new(
clamp_opt(
compression_level
.unwrap_or(bzip2::Compression::default().level() as i32),
bzip2_compression_level_range(),
)
.ok_or(ZipError::UnsupportedArchive(
"Unsupported compression level",
))? as u32,
),
)),
CompressionMethod::AES => {
return Err(ZipError::UnsupportedArchive(
"AES compression is not supported for writing",
))
}
#[cfg(feature = "zstd")]
CompressionMethod::Zstd => {
GenericZipWriter::Zstd(ZstdEncoder::new(bare, 0).unwrap())
}
CompressionMethod::Zstd => GenericZipWriter::Zstd(
ZstdEncoder::new(
bare,
clamp_opt(
compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL),
zstd::compression_level_range().clone(),
)
.ok_or(ZipError::UnsupportedArchive(
"Unsupported compression level",
))?,
)
.unwrap(),
),
CompressionMethod::Unsupported(..) => {
return Err(ZipError::UnsupportedArchive("Unsupported compression"))
}
@ -925,6 +987,26 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
}
}
fn deflate_compression_level_range() -> RangeInclusive<i32> {
let min = flate2::Compression::none().level() as i32;
let max = flate2::Compression::best().level() as i32;
min..=max
}
fn bzip2_compression_level_range() -> RangeInclusive<i32> {
let min = bzip2::Compression::none().level() as i32;
let max = bzip2::Compression::best().level() as i32;
min..=max
}
fn clamp_opt<T: Ord + Copy>(value: T, range: RangeInclusive<T>) -> Option<T> {
if range.contains(&value) {
Some(value)
} else {
None
}
}
fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
// local file header signature
writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
@ -1258,6 +1340,7 @@ mod test {
let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
let options = FileOptions {
compression_method: CompressionMethod::Stored,
compression_level: None,
last_modified_time: DateTime::default(),
permissions: Some(33188),
large_file: false,