diff --git a/Cargo.toml b/Cargo.toml index 472daf364..7e8ecf36d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ time = { version = "0.3.34", optional = true, default-features = false, features zstd = { version = "0.13.1", optional = true, default-features = false } zopfli = { version = "0.8.0", optional = true } deflate64 = { version = "0.1.8", optional = true } +lzma-rs = { version = "0.3.0", optional = true } [target.'cfg(any(all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc"))'.dependencies] crossbeam-utils = "0.8.19" @@ -49,8 +50,9 @@ deflate-miniz = ["flate2/default"] deflate-zlib = ["flate2/zlib"] deflate-zlib-ng = ["flate2/zlib-ng"] deflate-zopfli = ["zopfli"] +lzma = ["lzma-rs/stream"] unreserved = [] -default = ["aes-crypto", "bzip2", "deflate", "deflate64", "deflate-zlib-ng", "deflate-zopfli", "time", "zstd"] +default = ["aes-crypto", "bzip2", "deflate", "deflate64", "deflate-zlib-ng", "deflate-zopfli", "lzma", "time", "zstd"] [[bench]] name = "read_entry" diff --git a/src/compression.rs b/src/compression.rs index 5352c4876..14be540ed 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -41,6 +41,9 @@ pub enum CompressionMethod { /// Compress the file using ZStandard #[cfg(feature = "zstd")] Zstd, + /// Compress the file using LZMA + #[cfg(feature = "lzma")] + Lzma, /// Unsupported compression method #[cfg_attr( not(fuzzing), @@ -83,7 +86,10 @@ impl CompressionMethod { pub const BZIP2: Self = CompressionMethod::Bzip2; #[cfg(not(feature = "bzip2"))] pub const BZIP2: Self = CompressionMethod::Unsupported(12); + #[cfg(not(feature = "lzma"))] pub const LZMA: Self = CompressionMethod::Unsupported(14); + #[cfg(feature = "lzma")] + pub const LZMA: Self = CompressionMethod::Lzma; pub const IBM_ZOS_CMPSC: Self = CompressionMethod::Unsupported(16); pub const IBM_TERSE: Self = CompressionMethod::Unsupported(18); pub const ZSTD_DEPRECATED: Self = CompressionMethod::Unsupported(20); @@ -123,6 +129,8 @@ impl CompressionMethod { 9 => CompressionMethod::Deflate64, #[cfg(feature = "bzip2")] 12 => CompressionMethod::Bzip2, + #[cfg(feature = "lzma")] + 14 => CompressionMethod::Lzma, #[cfg(feature = "zstd")] 93 => CompressionMethod::Zstd, #[cfg(feature = "aes-crypto")] @@ -157,6 +165,8 @@ impl CompressionMethod { CompressionMethod::Aes => 99, #[cfg(feature = "zstd")] CompressionMethod::Zstd => 93, + #[cfg(feature = "lzma")] + CompressionMethod::Lzma => 14, CompressionMethod::Unsupported(v) => v, } diff --git a/src/read.rs b/src/read.rs index c9291ab89..1eefca5a4 100644 --- a/src/read.rs +++ b/src/read.rs @@ -37,6 +37,9 @@ use zstd::stream::read::Decoder as ZstdDecoder; /// Provides high level API for reading from a stream. pub(crate) mod stream; +#[cfg(feature = "lzma")] +pub(crate) mod lzma; + // Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely pub(crate) mod zip_archive { use std::sync::Arc; @@ -81,6 +84,8 @@ pub(crate) mod zip_archive { use crate::result::ZipError::InvalidPassword; pub use zip_archive::ZipArchive; +#[cfg(feature = "lzma")] +use crate::read::lzma::LzmaReader; #[allow(clippy::large_enum_variant)] pub(crate) enum CryptoReader<'a> { @@ -147,6 +152,8 @@ pub(crate) enum ZipFileReader<'a> { Bzip2(Crc32Reader>>), #[cfg(feature = "zstd")] Zstd(Crc32Reader>>>), + #[cfg(feature = "lzma")] + Lzma(Crc32Reader>>), } impl<'a> Read for ZipFileReader<'a> { @@ -168,6 +175,8 @@ impl<'a> Read for ZipFileReader<'a> { ZipFileReader::Bzip2(r) => r.read(buf), #[cfg(feature = "zstd")] ZipFileReader::Zstd(r) => r.read(buf), + #[cfg(feature = "lzma")] + ZipFileReader::Lzma(r) => r.read(buf) } } } @@ -192,6 +201,11 @@ impl<'a> ZipFileReader<'a> { ZipFileReader::Bzip2(r) => r.into_inner().into_inner().into_inner(), #[cfg(feature = "zstd")] ZipFileReader::Zstd(r) => r.into_inner().finish().into_inner().into_inner(), + #[cfg(feature = "lzma")] + ZipFileReader::Lzma(r) => { + let inner: Box<_> = r.into_inner().finish().unwrap().into(); + Read::take(Box::leak(inner), u64::MAX) + } } } } @@ -313,6 +327,11 @@ pub(crate) fn make_reader( let zstd_reader = ZstdDecoder::new(reader).unwrap(); ZipFileReader::Zstd(Crc32Reader::new(zstd_reader, crc32, ae2_encrypted)) } + #[cfg(feature = "lzma")] + CompressionMethod::Lzma => { + let reader = LzmaReader::new(reader); + ZipFileReader::Lzma(Crc32Reader::new(reader, crc32, ae2_encrypted)) + } _ => panic!("Compression method not supported"), } } diff --git a/src/read/lzma.rs b/src/read/lzma.rs new file mode 100644 index 000000000..5a7c41ac9 --- /dev/null +++ b/src/read/lzma.rs @@ -0,0 +1,47 @@ +use std::collections::VecDeque; +use std::io::{copy, Error, Read, Result, Write}; +use lzma_rs::decompress::{Options, Stream, UnpackedSize}; + +const COMPRESSED_BYTES_TO_BUFFER: usize = 4096; + +const OPTIONS: Options = Options { + unpacked_size: UnpackedSize::ReadFromHeader, + memlimit: None, + allow_incomplete: true, +}; + +#[derive(Debug)] +pub struct LzmaReader { + compressed_reader: R, + stream: Stream> +} + +impl LzmaReader { + pub fn new(inner: R) -> Self { + LzmaReader { + compressed_reader: inner, + stream: Stream::new_with_options(&OPTIONS, VecDeque::new()) + } + } + + pub fn finish(mut self) -> Result> { + copy(&mut self.compressed_reader, &mut self.stream)?; + self.stream.finish().map_err(Error::from) + } +} + +impl Read for LzmaReader { + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut bytes_read = self.stream.get_output_mut().unwrap().read(buf)?; + while bytes_read < buf.len() { + let mut next_compressed = [0u8; COMPRESSED_BYTES_TO_BUFFER]; + let compressed_bytes_read = self.compressed_reader.read(&mut next_compressed)?; + if compressed_bytes_read == 0 { + break; + } + self.stream.write_all(&next_compressed[..compressed_bytes_read])?; + bytes_read += self.stream.get_output_mut().unwrap().read(&mut buf[bytes_read..])?; + } + Ok(bytes_read) + } +} \ No newline at end of file diff --git a/src/write.rs b/src/write.rs index 82305e4a3..21d6f4977 100644 --- a/src/write.rs +++ b/src/write.rs @@ -129,7 +129,7 @@ pub(crate) mod zip_writer { pub(super) flush_on_finish_file: bool, } } -use crate::result::ZipError::InvalidArchive; +use crate::result::ZipError::{InvalidArchive, UnsupportedArchive}; use crate::write::GenericZipWriter::{Closed, Storer}; use crate::zipcrypto::ZipCryptoKeys; use crate::CompressionMethod::Stored; @@ -1269,6 +1269,8 @@ impl GenericZipWriter { GenericZipWriter::Zstd(ZstdEncoder::new(bare, level as i32).unwrap()) })) } + #[cfg(feature = "lzma")] + CompressionMethod::Lzma => Err(UnsupportedArchive("LZMA isn't supported for compression")), CompressionMethod::Unsupported(..) => { Err(ZipError::UnsupportedArchive("Unsupported compression")) }