zip/
compression.rs

1//! Possible ZIP compression methods.
2
3use core::fmt;
4use std::io;
5
6#[allow(deprecated)]
7/// Identifies the storage format used to compress a file within a ZIP archive.
8///
9/// Each file's compression method is stored alongside it, allowing the
10/// contents to be read without context.
11///
12/// When creating ZIP files, you may choose the method to use with
13/// [`crate::write::FileOptions::compression_method`]
14#[derive(Copy, Clone, PartialEq, Eq, Debug)]
15#[cfg_attr(feature = "_arbitrary", derive(arbitrary::Arbitrary))]
16#[non_exhaustive]
17pub enum CompressionMethod {
18    /// Store the file as is
19    Stored,
20    /// Compress the file using Deflate
21    #[cfg(feature = "_deflate-any")]
22    Deflated,
23    /// Compress the file using Deflate64.
24    /// Decoding deflate64 is supported but encoding deflate64 is not supported.
25    #[cfg(feature = "deflate64")]
26    Deflate64,
27    /// Compress the file using BZIP2
28    #[cfg(feature = "bzip2")]
29    Bzip2,
30    /// Encrypted using AES.
31    ///
32    /// The actual compression method has to be taken from the AES extra data field
33    /// or from `ZipFileData`.
34    #[cfg(feature = "aes-crypto")]
35    Aes,
36    /// Compress the file using ZStandard
37    #[cfg(feature = "zstd")]
38    Zstd,
39    /// Compress the file using LZMA
40    #[cfg(feature = "lzma")]
41    Lzma,
42    #[cfg(feature = "legacy-zip")]
43    /// Method 1 Shrink
44    Shrink,
45    #[cfg(feature = "legacy-zip")]
46    /// Reduce (Method 2-5)
47    Reduce(u8),
48    #[cfg(feature = "legacy-zip")]
49    /// Method 6 Implode/explode
50    Implode,
51    /// Compress the file using XZ
52    #[cfg(feature = "xz")]
53    Xz,
54    /// Compress the file using PPMd
55    #[cfg(feature = "ppmd")]
56    Ppmd,
57    /// Unsupported compression method
58    #[cfg_attr(
59        not(any(fuzzing, feature = "_arbitrary")),
60        deprecated(since = "0.5.7", note = "use the constants instead")
61    )]
62    Unsupported(u16),
63}
64#[allow(deprecated, missing_docs)]
65/// All compression methods defined for the ZIP format
66impl CompressionMethod {
67    pub const STORE: Self = CompressionMethod::Stored;
68    #[cfg(feature = "legacy-zip")]
69    pub const SHRINK: Self = CompressionMethod::Shrink;
70    #[cfg(not(feature = "legacy-zip"))]
71    /// Legacy compression method (enable feature `legacy-zip` to get support)
72    pub const SHRINK: Self = CompressionMethod::Unsupported(1);
73    #[cfg(feature = "legacy-zip")]
74    /// Legacy compression method
75    pub const REDUCE_1: Self = CompressionMethod::Reduce(1);
76    #[cfg(not(feature = "legacy-zip"))]
77    /// Legacy compression method (enable feature `legacy-zip` to get support)
78    pub const REDUCE_1: Self = CompressionMethod::Unsupported(2);
79    #[cfg(feature = "legacy-zip")]
80    /// Legacy compression method
81    pub const REDUCE_2: Self = CompressionMethod::Reduce(2);
82    #[cfg(not(feature = "legacy-zip"))]
83    /// Legacy compression method (enable feature `legacy-zip` to get support)
84    pub const REDUCE_2: Self = CompressionMethod::Unsupported(3);
85    #[cfg(feature = "legacy-zip")]
86    /// Legacy compression method
87    pub const REDUCE_3: Self = CompressionMethod::Reduce(3);
88    #[cfg(not(feature = "legacy-zip"))]
89    /// Legacy compression method (enable feature `legacy-zip` to get support)
90    pub const REDUCE_3: Self = CompressionMethod::Unsupported(4);
91    #[cfg(feature = "legacy-zip")]
92    /// Legacy compression method
93    pub const REDUCE_4: Self = CompressionMethod::Reduce(4);
94    #[cfg(not(feature = "legacy-zip"))]
95    /// Legacy compression method (enable feature `legacy-zip` to get support)
96    pub const REDUCE_4: Self = CompressionMethod::Unsupported(5);
97    #[cfg(feature = "legacy-zip")]
98    /// Legacy compression method
99    pub const IMPLODE: Self = CompressionMethod::Implode;
100    #[cfg(not(feature = "legacy-zip"))]
101    /// Legacy compression method (enable feature `legacy-zip` to get support)
102    pub const IMPLODE: Self = CompressionMethod::Unsupported(6);
103    #[cfg(feature = "_deflate-any")]
104    pub const DEFLATE: Self = CompressionMethod::Deflated;
105    #[cfg(not(feature = "_deflate-any"))]
106    pub const DEFLATE: Self = CompressionMethod::Unsupported(8);
107    #[cfg(feature = "deflate64")]
108    pub const DEFLATE64: Self = CompressionMethod::Deflate64;
109    #[cfg(not(feature = "deflate64"))]
110    pub const DEFLATE64: Self = CompressionMethod::Unsupported(9);
111    pub const PKWARE_IMPLODE: Self = CompressionMethod::Unsupported(10);
112    #[cfg(feature = "bzip2")]
113    pub const BZIP2: Self = CompressionMethod::Bzip2;
114    #[cfg(not(feature = "bzip2"))]
115    pub const BZIP2: Self = CompressionMethod::Unsupported(12);
116    #[cfg(not(feature = "lzma"))]
117    pub const LZMA: Self = CompressionMethod::Unsupported(14);
118    #[cfg(feature = "lzma")]
119    pub const LZMA: Self = CompressionMethod::Lzma;
120    pub const IBM_ZOS_CMPSC: Self = CompressionMethod::Unsupported(16);
121    pub const IBM_TERSE: Self = CompressionMethod::Unsupported(18);
122    pub const ZSTD_DEPRECATED: Self = CompressionMethod::Unsupported(20);
123    #[cfg(feature = "zstd")]
124    pub const ZSTD: Self = CompressionMethod::Zstd;
125    #[cfg(not(feature = "zstd"))]
126    pub const ZSTD: Self = CompressionMethod::Unsupported(93);
127    pub const MP3: Self = CompressionMethod::Unsupported(94);
128    #[cfg(feature = "xz")]
129    pub const XZ: Self = CompressionMethod::Xz;
130    #[cfg(not(feature = "xz"))]
131    pub const XZ: Self = CompressionMethod::Unsupported(95);
132    pub const JPEG: Self = CompressionMethod::Unsupported(96);
133    pub const WAVPACK: Self = CompressionMethod::Unsupported(97);
134    #[cfg(feature = "ppmd")]
135    pub const PPMD: Self = CompressionMethod::Ppmd;
136    #[cfg(not(feature = "ppmd"))]
137    pub const PPMD: Self = CompressionMethod::Unsupported(98);
138    #[cfg(feature = "aes-crypto")]
139    pub const AES: Self = CompressionMethod::Aes;
140    #[cfg(not(feature = "aes-crypto"))]
141    pub const AES: Self = CompressionMethod::Unsupported(99);
142
143    #[cfg(feature = "_deflate-any")]
144    pub const DEFAULT: Self = CompressionMethod::Deflated;
145
146    #[cfg(all(not(feature = "_deflate-any"), feature = "bzip2"))]
147    pub const DEFAULT: Self = CompressionMethod::Bzip2;
148
149    #[cfg(all(not(feature = "_deflate-any"), not(feature = "bzip2")))]
150    pub const DEFAULT: Self = CompressionMethod::Stored;
151}
152impl CompressionMethod {
153    pub(crate) const fn parse_from_u16(val: u16) -> Self {
154        match val {
155            0 => CompressionMethod::Stored,
156            #[cfg(feature = "legacy-zip")]
157            1 => CompressionMethod::Shrink,
158            #[cfg(feature = "legacy-zip")]
159            2 => CompressionMethod::Reduce(1),
160            #[cfg(feature = "legacy-zip")]
161            3 => CompressionMethod::Reduce(2),
162            #[cfg(feature = "legacy-zip")]
163            4 => CompressionMethod::Reduce(3),
164            #[cfg(feature = "legacy-zip")]
165            5 => CompressionMethod::Reduce(4),
166            #[cfg(feature = "legacy-zip")]
167            6 => CompressionMethod::Implode,
168            #[cfg(feature = "_deflate-any")]
169            8 => CompressionMethod::Deflated,
170            #[cfg(feature = "deflate64")]
171            9 => CompressionMethod::Deflate64,
172            #[cfg(feature = "bzip2")]
173            12 => CompressionMethod::Bzip2,
174            #[cfg(feature = "lzma")]
175            14 => CompressionMethod::Lzma,
176            #[cfg(feature = "xz")]
177            95 => CompressionMethod::Xz,
178            #[cfg(feature = "zstd")]
179            93 => CompressionMethod::Zstd,
180            #[cfg(feature = "ppmd")]
181            98 => CompressionMethod::Ppmd,
182            #[cfg(feature = "aes-crypto")]
183            99 => CompressionMethod::Aes,
184            #[allow(deprecated)]
185            v => CompressionMethod::Unsupported(v),
186        }
187    }
188
189    /// Converts a u16 to its corresponding CompressionMethod
190    #[deprecated(
191        since = "0.5.7",
192        note = "use a constant to construct a compression method"
193    )]
194    pub const fn from_u16(val: u16) -> CompressionMethod {
195        Self::parse_from_u16(val)
196    }
197
198    pub(crate) const fn serialize_to_u16(self) -> u16 {
199        match self {
200            CompressionMethod::Stored => 0,
201            #[cfg(feature = "legacy-zip")]
202            CompressionMethod::Shrink => 1,
203            #[cfg(feature = "legacy-zip")]
204            CompressionMethod::Reduce(n) => 1 + n as u16,
205            #[cfg(feature = "legacy-zip")]
206            CompressionMethod::Implode => 6,
207
208            #[cfg(feature = "_deflate-any")]
209            CompressionMethod::Deflated => 8,
210            #[cfg(feature = "deflate64")]
211            CompressionMethod::Deflate64 => 9,
212            #[cfg(feature = "bzip2")]
213            CompressionMethod::Bzip2 => 12,
214            #[cfg(feature = "aes-crypto")]
215            CompressionMethod::Aes => 99,
216            #[cfg(feature = "zstd")]
217            CompressionMethod::Zstd => 93,
218            #[cfg(feature = "lzma")]
219            CompressionMethod::Lzma => 14,
220            #[cfg(feature = "xz")]
221            CompressionMethod::Xz => 95,
222            #[cfg(feature = "ppmd")]
223            CompressionMethod::Ppmd => 98,
224            #[allow(deprecated)]
225            CompressionMethod::Unsupported(v) => v,
226        }
227    }
228
229    /// Converts a CompressionMethod to a u16
230    #[deprecated(
231        since = "0.5.7",
232        note = "to match on other compression methods, use a constant"
233    )]
234    pub const fn to_u16(self) -> u16 {
235        self.serialize_to_u16()
236    }
237}
238
239impl Default for CompressionMethod {
240    fn default() -> Self {
241        Self::DEFAULT
242    }
243}
244
245impl fmt::Display for CompressionMethod {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        // Just duplicate what the Debug format looks like, i.e, the enum key:
248        write!(f, "{self:?}")
249    }
250}
251
252/// The compression methods which have been implemented.
253pub const SUPPORTED_COMPRESSION_METHODS: &[CompressionMethod] = &[
254    CompressionMethod::Stored,
255    #[cfg(feature = "_deflate-any")]
256    CompressionMethod::Deflated,
257    #[cfg(feature = "deflate64")]
258    CompressionMethod::Deflate64,
259    #[cfg(feature = "bzip2")]
260    CompressionMethod::Bzip2,
261    #[cfg(feature = "zstd")]
262    CompressionMethod::Zstd,
263    #[cfg(feature = "xz")]
264    CompressionMethod::Xz,
265    #[cfg(feature = "ppmd")]
266    CompressionMethod::Ppmd,
267];
268
269pub(crate) enum Decompressor<R: io::BufRead> {
270    Stored(R),
271    #[cfg(feature = "deflate-flate2")]
272    Deflated(flate2::bufread::DeflateDecoder<R>),
273    #[cfg(feature = "deflate64")]
274    Deflate64(deflate64::Deflate64Decoder<R>),
275    #[cfg(feature = "bzip2")]
276    Bzip2(bzip2::bufread::BzDecoder<R>),
277    #[cfg(feature = "zstd")]
278    Zstd(zstd::Decoder<'static, R>),
279    #[cfg(feature = "lzma")]
280    Lzma(Lzma<R>),
281    #[cfg(feature = "legacy-zip")]
282    Shrink(crate::legacy::shrink::ShrinkDecoder<R>),
283    #[cfg(feature = "legacy-zip")]
284    Reduce(crate::legacy::reduce::ReduceDecoder<R>),
285    #[cfg(feature = "legacy-zip")]
286    Implode(crate::legacy::implode::ImplodeDecoder<R>),
287    #[cfg(feature = "xz")]
288    Xz(Box<lzma_rust2::XzReader<R>>),
289    #[cfg(feature = "ppmd")]
290    Ppmd(Ppmd<R>),
291}
292
293#[cfg(feature = "lzma")]
294pub(crate) enum Lzma<R: io::BufRead> {
295    Uninitialized {
296        reader: Option<R>,
297        uncompressed_size: u64,
298    },
299    Initialized(Box<lzma_rust2::LzmaReader<R>>),
300}
301
302#[cfg(feature = "ppmd")]
303pub(crate) enum Ppmd<R: io::BufRead> {
304    Uninitialized(Option<R>),
305    Initialized(Box<ppmd_rust::Ppmd8Decoder<R>>),
306}
307
308impl<R: io::BufRead> io::Read for Decompressor<R> {
309    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
310        match self {
311            Decompressor::Stored(r) => r.read(buf),
312            #[cfg(feature = "deflate-flate2")]
313            Decompressor::Deflated(r) => r.read(buf),
314            #[cfg(feature = "deflate64")]
315            Decompressor::Deflate64(r) => r.read(buf),
316            #[cfg(feature = "bzip2")]
317            Decompressor::Bzip2(r) => r.read(buf),
318            #[cfg(feature = "zstd")]
319            Decompressor::Zstd(r) => r.read(buf),
320            #[cfg(feature = "lzma")]
321            Decompressor::Lzma(r) => match r {
322                Lzma::Uninitialized {
323                    reader,
324                    uncompressed_size,
325                } => {
326                    let mut reader = reader.take().ok_or_else(|| {
327                        io::Error::other("Reader was not set while reading LZMA data")
328                    })?;
329
330                    // 5.8.8.1 LZMA Version Information & 5.8.8.2 LZMA Properties Size
331                    let mut header = [0; 4];
332                    reader.read_exact(&mut header)?;
333                    let _version_information = u16::from_le_bytes(header[0..2].try_into().unwrap());
334                    let properties_size = u16::from_le_bytes(header[2..4].try_into().unwrap());
335                    if properties_size != 5 {
336                        return Err(io::Error::new(
337                            io::ErrorKind::InvalidInput,
338                            format!("unexpected LZMA properties size of {properties_size}"),
339                        ));
340                    }
341
342                    let mut props_data = [0; 5];
343                    reader.read_exact(&mut props_data)?;
344                    let props = props_data[0];
345                    let dict_size = u32::from_le_bytes(props_data[1..5].try_into().unwrap());
346
347                    // We don't need to handle the end-of-stream marker here, since the LZMA reader
348                    // stops at the end-of-stream marker OR when it has decoded uncompressed_size bytes, whichever comes first.
349                    let mut decompressor = lzma_rust2::LzmaReader::new_with_props(
350                        reader,
351                        *uncompressed_size,
352                        props,
353                        dict_size,
354                        None,
355                    )?;
356
357                    let read = decompressor.read(buf)?;
358
359                    *r = Lzma::Initialized(Box::new(decompressor));
360
361                    Ok(read)
362                }
363                Lzma::Initialized(decompressor) => decompressor.read(buf),
364            },
365            #[cfg(feature = "xz")]
366            Decompressor::Xz(r) => r.read(buf),
367            #[cfg(feature = "ppmd")]
368            Decompressor::Ppmd(r) => match r {
369                Ppmd::Uninitialized(reader) => {
370                    let mut reader = reader.take().ok_or_else(|| {
371                        io::Error::other("Reader was not set while reading PPMd data")
372                    })?;
373
374                    let mut buffer = [0; 2];
375                    reader.read_exact(&mut buffer)?;
376                    let parameters = u16::from_le_bytes(buffer);
377
378                    let order = ((parameters & 0x0F) + 1) as u32;
379                    let memory_size = 1024 * 1024 * (((parameters >> 4) & 0xFF) + 1) as u32;
380                    let restoration_method = (parameters >> 12) & 0x0F;
381
382                    let mut decompressor = ppmd_rust::Ppmd8Decoder::new(
383                        reader,
384                        order,
385                        memory_size,
386                        restoration_method.into(),
387                    )
388                    .map_err(|error| match error {
389                        ppmd_rust::Error::RangeDecoderInitialization => io::Error::new(
390                            io::ErrorKind::InvalidData,
391                            "PPMd range coder initialization failed",
392                        ),
393                        ppmd_rust::Error::InvalidParameter => {
394                            io::Error::new(io::ErrorKind::InvalidInput, "Invalid PPMd parameter")
395                        }
396                        ppmd_rust::Error::IoError(io_error) => io_error,
397                        ppmd_rust::Error::MemoryAllocation => {
398                            io::Error::new(io::ErrorKind::OutOfMemory, "Memory allocation failed")
399                        }
400                    })?;
401
402                    let read = decompressor.read(buf)?;
403
404                    *r = Ppmd::Initialized(Box::new(decompressor));
405
406                    Ok(read)
407                }
408                Ppmd::Initialized(decompressor) => decompressor.read(buf),
409            },
410            #[cfg(feature = "legacy-zip")]
411            Decompressor::Shrink(r) => r.read(buf),
412            #[cfg(feature = "legacy-zip")]
413            Decompressor::Reduce(r) => r.read(buf),
414            #[cfg(feature = "legacy-zip")]
415            Decompressor::Implode(r) => r.read(buf),
416        }
417    }
418}
419
420impl<R: io::BufRead> Decompressor<R> {
421    pub fn new(
422        reader: R,
423        compression_method: CompressionMethod,
424        #[cfg(any(feature = "lzma", feature = "legacy-zip"))] uncompressed_size: u64,
425        #[cfg(not(any(feature = "lzma", feature = "legacy-zip")))] _uncompressed_size: u64,
426        #[cfg(feature = "legacy-zip")] flags: u16,
427        #[cfg(not(feature = "legacy-zip"))] _flags: u16,
428    ) -> crate::result::ZipResult<Self> {
429        Ok(match compression_method {
430            CompressionMethod::Stored => Decompressor::Stored(reader),
431            #[cfg(feature = "deflate-flate2")]
432            CompressionMethod::Deflated => {
433                Decompressor::Deflated(flate2::bufread::DeflateDecoder::new(reader))
434            }
435            #[cfg(feature = "deflate64")]
436            CompressionMethod::Deflate64 => {
437                Decompressor::Deflate64(deflate64::Deflate64Decoder::with_buffer(reader))
438            }
439            #[cfg(feature = "bzip2")]
440            CompressionMethod::Bzip2 => Decompressor::Bzip2(bzip2::bufread::BzDecoder::new(reader)),
441            #[cfg(feature = "zstd")]
442            CompressionMethod::Zstd => Decompressor::Zstd(zstd::Decoder::with_buffer(reader)?),
443            #[cfg(feature = "lzma")]
444            CompressionMethod::Lzma => Decompressor::Lzma(Lzma::Uninitialized {
445                reader: Some(reader),
446                uncompressed_size,
447            }),
448            #[cfg(feature = "xz")]
449            CompressionMethod::Xz => {
450                Decompressor::Xz(Box::new(lzma_rust2::XzReader::new(reader, false)))
451            }
452            #[cfg(feature = "ppmd")]
453            CompressionMethod::Ppmd => Decompressor::Ppmd(Ppmd::Uninitialized(Some(reader))),
454            #[cfg(feature = "legacy-zip")]
455            CompressionMethod::Shrink => Decompressor::Shrink(
456                crate::legacy::shrink::ShrinkDecoder::new(reader, uncompressed_size),
457            ),
458            #[cfg(feature = "legacy-zip")]
459            CompressionMethod::Reduce(n) => Decompressor::Reduce(
460                crate::legacy::reduce::ReduceDecoder::new(reader, uncompressed_size, n),
461            ),
462            #[cfg(feature = "legacy-zip")]
463            CompressionMethod::Implode => Decompressor::Implode(
464                crate::legacy::implode::ImplodeDecoder::new(reader, uncompressed_size, flags),
465            ),
466            _ => {
467                return Err(crate::result::ZipError::UnsupportedArchive(
468                    "Compression method not supported",
469                ))
470            }
471        })
472    }
473
474    /// Consumes this decoder, returning the underlying reader.
475    #[allow(clippy::infallible_destructuring_match)]
476    pub fn into_inner(self) -> io::Result<R> {
477        let inner = match self {
478            Decompressor::Stored(r) => r,
479            #[cfg(feature = "deflate-flate2")]
480            Decompressor::Deflated(r) => r.into_inner(),
481            #[cfg(feature = "deflate64")]
482            Decompressor::Deflate64(r) => r.into_inner(),
483            #[cfg(feature = "bzip2")]
484            Decompressor::Bzip2(r) => r.into_inner(),
485            #[cfg(feature = "zstd")]
486            Decompressor::Zstd(r) => r.finish(),
487            #[cfg(feature = "lzma")]
488            Decompressor::Lzma(r) => match r {
489                Lzma::Uninitialized { mut reader, .. } => reader
490                    .take()
491                    .ok_or_else(|| io::Error::other("Reader was not set"))?,
492                Lzma::Initialized(decoder) => decoder.into_inner(),
493            },
494            #[cfg(feature = "legacy-zip")]
495            Decompressor::Shrink(r) => r.into_inner(),
496            #[cfg(feature = "legacy-zip")]
497            Decompressor::Reduce(r) => r.into_inner(),
498            #[cfg(feature = "legacy-zip")]
499            Decompressor::Implode(r) => r.into_inner(),
500            #[cfg(feature = "xz")]
501            Decompressor::Xz(r) => r.into_inner(),
502            #[cfg(feature = "ppmd")]
503            Decompressor::Ppmd(r) => match r {
504                Ppmd::Uninitialized(mut reader) => reader
505                    .take()
506                    .ok_or_else(|| io::Error::other("Reader was not set"))?,
507                Ppmd::Initialized(decoder) => decoder.into_inner(),
508            },
509        };
510        Ok(inner)
511    }
512}
513
514#[cfg(test)]
515mod test {
516    use super::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
517
518    #[test]
519    fn from_eq_to() {
520        for v in 0..(u16::MAX as u32 + 1) {
521            let from = CompressionMethod::parse_from_u16(v as u16);
522            let to = from.serialize_to_u16() as u32;
523            assert_eq!(v, to);
524        }
525    }
526
527    #[test]
528    fn to_eq_from() {
529        fn check_match(method: CompressionMethod) {
530            let to = method.serialize_to_u16();
531            let from = CompressionMethod::parse_from_u16(to);
532            let back = from.serialize_to_u16();
533            assert_eq!(to, back);
534        }
535
536        for &method in SUPPORTED_COMPRESSION_METHODS {
537            check_match(method);
538        }
539    }
540
541    #[test]
542    fn to_display_fmt() {
543        fn check_match(method: CompressionMethod) {
544            let debug_str = format!("{method:?}");
545            let display_str = format!("{method}");
546            assert_eq!(debug_str, display_str);
547        }
548
549        for &method in SUPPORTED_COMPRESSION_METHODS {
550            check_match(method);
551        }
552    }
553}