alpm_compress/decompression/
decoder.rs

1//! Decoder for decompression which supports multiple backends.
2
3use std::{
4    fmt::Debug,
5    fs::File,
6    io::{BufReader, Read},
7};
8
9use bzip2::bufread::BzDecoder;
10use flate2::bufread::GzDecoder;
11use liblzma::bufread::XzDecoder;
12use zstd::Decoder;
13
14use crate::{Error, decompression::DecompressionSettings};
15
16/// Decoder for decompression which supports multiple backends.
17///
18/// Wraps [`BzDecoder`], [`GzDecoder`], [`XzDecoder`] and [`Decoder`]
19/// and provides a unified [`Read`] implementation across all of them.
20pub enum CompressionDecoder<'a> {
21    /// The bzip2 decompression decoder.
22    Bzip2(BzDecoder<BufReader<File>>),
23
24    /// The gzip decompression decoder.
25    Gzip(GzDecoder<BufReader<File>>),
26
27    /// The xz decompression decoder.
28    Xz(XzDecoder<BufReader<File>>),
29
30    /// The zstd decompression decoder.
31    Zstd(Decoder<'a, BufReader<File>>),
32
33    /// No compression.
34    None(BufReader<File>),
35}
36
37impl CompressionDecoder<'_> {
38    /// Creates a new [`CompressionDecoder`].
39    ///
40    /// Uses a [`File`] to stream from and initializes a specific backend based on the provided
41    /// [`DecompressionSettings`].
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if creating the decoder for zstd compression fails
46    /// (all other decoder initializations are infallible).
47    pub fn new(file: File, settings: DecompressionSettings) -> Result<Self, Error> {
48        match settings {
49            DecompressionSettings::Bzip2 => Ok(Self::Bzip2(BzDecoder::new(BufReader::new(file)))),
50            DecompressionSettings::Gzip => Ok(Self::Gzip(GzDecoder::new(BufReader::new(file)))),
51            DecompressionSettings::Xz => Ok(Self::Xz(XzDecoder::new(BufReader::new(file)))),
52            DecompressionSettings::Zstd => Ok(Self::Zstd(
53                Decoder::new(file).map_err(Error::CreateZstandardDecoder)?,
54            )),
55            DecompressionSettings::None => Ok(Self::None(BufReader::new(file))),
56        }
57    }
58}
59
60impl Debug for CompressionDecoder<'_> {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(
63            f,
64            "CompressionDecoder({})",
65            match self {
66                CompressionDecoder::Bzip2(_) => "Bzip2",
67                CompressionDecoder::Gzip(_) => "Gzip",
68                CompressionDecoder::Xz(_) => "Xz",
69                CompressionDecoder::Zstd(_) => "Zstd",
70                CompressionDecoder::None(_) => "None",
71            }
72        )
73    }
74}
75
76impl Read for CompressionDecoder<'_> {
77    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
78        match self {
79            CompressionDecoder::Bzip2(decoder) => decoder.read(buf),
80            CompressionDecoder::Gzip(decoder) => decoder.read(buf),
81            CompressionDecoder::Xz(decoder) => decoder.read(buf),
82            CompressionDecoder::Zstd(decoder) => decoder.read(buf),
83            CompressionDecoder::None(reader) => reader.read(buf),
84        }
85    }
86
87    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
88        match self {
89            CompressionDecoder::Bzip2(decoder) => decoder.read_to_end(buf),
90            CompressionDecoder::Gzip(decoder) => decoder.read_to_end(buf),
91            CompressionDecoder::Xz(decoder) => decoder.read_to_end(buf),
92            CompressionDecoder::Zstd(decoder) => decoder.read_to_end(buf),
93            CompressionDecoder::None(reader) => reader.read_to_end(buf),
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use std::io::{Seek, Write};
101
102    use rstest::rstest;
103    use tempfile::tempfile;
104    use testresult::TestResult;
105
106    use super::*;
107    use crate::compression::{
108        Bzip2CompressionLevel,
109        CompressionEncoder,
110        CompressionSettings,
111        GzipCompressionLevel,
112        XzCompressionLevel,
113        ZstdCompressionLevel,
114        ZstdThreads,
115    };
116
117    /// Ensures that the [`CompressionDecoder`] can decompress data compressed by
118    /// [`CompressionEncoder`].
119    #[rstest]
120    #[case::bzip2(DecompressionSettings::Bzip2, CompressionSettings::Bzip2 {
121        compression_level: Bzip2CompressionLevel::default()
122    })]
123    #[case::gzip(DecompressionSettings::Gzip, CompressionSettings::Gzip {
124        compression_level: GzipCompressionLevel::default()
125    })]
126    #[case::xz(DecompressionSettings::Xz, CompressionSettings::Xz {
127        compression_level: XzCompressionLevel::default()
128    })]
129    #[case::zstd(DecompressionSettings::Zstd, CompressionSettings::Zstd {
130        compression_level: ZstdCompressionLevel::default(),
131        threads: ZstdThreads::new(0),
132    })]
133    #[case::no_compression(DecompressionSettings::None, CompressionSettings::None)]
134    fn test_compression_decoder_roundtrip(
135        #[case] decompression_settings: DecompressionSettings,
136        #[case] compression_settings: CompressionSettings,
137    ) -> TestResult {
138        // Prepare some sample data
139        let input_data = b"alpm4ever";
140
141        // Compress it
142        let mut file = tempfile()?;
143        {
144            let mut encoder = CompressionEncoder::new(file.try_clone()?, &compression_settings)?;
145            encoder.write_all(input_data)?;
146            encoder.flush()?;
147            encoder.finish()?;
148        }
149
150        // Rewind the file
151        file.rewind()?;
152
153        // Decompress it
154        let mut decoder = CompressionDecoder::new(file, decompression_settings)?;
155        let mut output = Vec::new();
156        decoder.read_to_end(&mut output)?;
157
158        // Check data integrity
159        assert_eq!(output, input_data);
160        Ok(())
161    }
162
163    #[rstest]
164    #[case::bzip2(DecompressionSettings::Bzip2)]
165    #[case::gzip(DecompressionSettings::Gzip)]
166    #[case::xz(DecompressionSettings::Xz)]
167    #[case::zstd(DecompressionSettings::Zstd)]
168    #[case::no_compression(DecompressionSettings::None)]
169    fn test_compression_decoder_debug(#[case] settings: DecompressionSettings) -> TestResult {
170        let file = tempfile()?;
171        let decoder = CompressionDecoder::new(file, settings)?;
172        let debug_str = format!("{decoder:?}");
173        assert!(debug_str.contains("CompressionDecoder"));
174        Ok(())
175    }
176}