zip/
compression.rs

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