From 7f8311efeaebe04d67b92757cc5eaae890b1794f Mon Sep 17 00:00:00 2001
From: Chris Hennick <hennickc@amazon.com>
Date: Thu, 11 Apr 2024 13:03:57 -0700
Subject: [PATCH] Add support for decompressing LZMA

---
 Cargo.toml         |  4 +++-
 src/compression.rs | 10 ++++++++++
 src/read.rs        | 19 +++++++++++++++++++
 src/read/lzma.rs   | 47 ++++++++++++++++++++++++++++++++++++++++++++++
 src/write.rs       |  4 +++-
 5 files changed, 82 insertions(+), 2 deletions(-)
 create mode 100644 src/read/lzma.rs

diff --git a/Cargo.toml b/Cargo.toml
index 472daf36..7e8ecf36 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 5352c487..14be540e 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 c9291ab8..1eefca5a 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<BzDecoder<CryptoReader<'a>>>),
     #[cfg(feature = "zstd")]
     Zstd(Crc32Reader<ZstdDecoder<'a, io::BufReader<CryptoReader<'a>>>>),
+    #[cfg(feature = "lzma")]
+    Lzma(Crc32Reader<LzmaReader<CryptoReader<'a>>>),
 }
 
 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 00000000..5a7c41ac
--- /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<R> {
+    compressed_reader: R,
+    stream: Stream<VecDeque<u8>>
+}
+
+impl <R: Read> LzmaReader<R> {
+    pub fn new(inner: R) -> Self {
+        LzmaReader {
+            compressed_reader: inner,
+            stream: Stream::new_with_options(&OPTIONS, VecDeque::new())
+        }
+    }
+    
+    pub fn finish(mut self) -> Result<VecDeque<u8>> {
+        copy(&mut self.compressed_reader, &mut self.stream)?;
+        self.stream.finish().map_err(Error::from)
+    }
+}
+
+impl <R: Read> Read for LzmaReader<R> {
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
+        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 82305e4a..21d6f497 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<W: Write + Seek> GenericZipWriter<W> {
                         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"))
                 }