alpm_package/
compression.rs

1//! Compression handling.
2
3use std::{
4    fmt::{Debug, Display},
5    fs::File,
6    io::{BufReader, Read, Write},
7    num::TryFromIntError,
8};
9
10use alpm_types::CompressionAlgorithmFileExtension;
11use bzip2::{bufread::BzDecoder, write::BzEncoder};
12use flate2::{bufread::GzDecoder, write::GzEncoder};
13use liblzma::{bufread::XzDecoder, write::XzEncoder};
14use log::trace;
15use zstd::{Decoder, Encoder};
16
17/// An error that can occur when using compression.
18#[derive(Debug, thiserror::Error)]
19pub enum Error {
20    /// An error occurred while creating a Zstandard encoder.
21    #[error(
22        "Error creating a Zstandard encoder while {context} with {compression_settings:?}:\n{source}"
23    )]
24    CreateZstandardEncoder {
25        /// The context in which the error occurred.
26        ///
27        /// This is meant to complete the sentence "Error creating a Zstandard encoder while
28        /// {context} with {compression_settings}".
29        context: &'static str,
30        /// The compression settings chosen for the encoder.
31        compression_settings: CompressionSettings,
32        /// The source error.
33        source: std::io::Error,
34    },
35
36    /// An error occurred while creating a Zstandard decoder.
37    #[error("Error creating a Zstandard decoder:\n{0}")]
38    CreateZstandardDecoder(#[source] std::io::Error),
39
40    /// An error occurred while finishing a compression encoder.
41    #[error("Error while finishing {compression_type} compression encoder:\n{source}")]
42    FinishEncoder {
43        /// The compression chosen for the encoder.
44        compression_type: CompressionAlgorithmFileExtension,
45        /// The source error
46        source: std::io::Error,
47    },
48
49    /// An error occurred while trying to get the available parallelism.
50    #[error("Error while trying to get available parallelism:\n{0}")]
51    GetParallelism(#[source] std::io::Error),
52
53    /// An error occurred while trying to convert an integer.
54    #[error("Error while trying to convert an integer:\n{0}")]
55    IntegerConversion(#[source] TryFromIntError),
56
57    /// A compression level is not valid.
58    #[error("Invalid compression level {level} (must be in the range {min} - {max})")]
59    InvalidCompressionLevel {
60        /// The invalid compression level.
61        level: u8,
62        /// The minimum valid compression level.
63        min: u8,
64        /// The maximum valid compression level.
65        max: u8,
66    },
67
68    /// An unsupported compression algorithm was used.
69    #[error("Unsupported compression algorithm: {value}")]
70    UnsupportedCompressionAlgorithm {
71        /// The unsupported compression algorithm.
72        value: String,
73    },
74}
75
76/// A supported compression algorithm.
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum CompressionAlgorithm {
79    /// The bzip2 compression algorithm.
80    Bzip2,
81    /// The gzip compression algorithm.
82    Gzip,
83    /// The xz compression algorithm.
84    Xz,
85    /// The zstandard compression algorithm.
86    Zstd,
87}
88
89impl TryFrom<CompressionAlgorithmFileExtension> for CompressionAlgorithm {
90    type Error = Error;
91
92    /// Converts a [`CompressionAlgorithmFileExtension`] into a [`CompressionAlgorithm`].
93    fn try_from(value: CompressionAlgorithmFileExtension) -> Result<Self, Self::Error> {
94        match value {
95            CompressionAlgorithmFileExtension::Bzip2 => Ok(Self::Bzip2),
96            CompressionAlgorithmFileExtension::Gzip => Ok(Self::Gzip),
97            CompressionAlgorithmFileExtension::Xz => Ok(Self::Xz),
98            CompressionAlgorithmFileExtension::Zstd => Ok(Self::Zstd),
99            _ => Err(Error::UnsupportedCompressionAlgorithm {
100                value: value.to_string(),
101            }),
102        }
103    }
104}
105
106impl From<&CompressionSettings> for CompressionAlgorithm {
107    /// Converts a [`CompressionSettings`] into a [`CompressionAlgorithm`].
108    fn from(value: &CompressionSettings) -> Self {
109        match value {
110            CompressionSettings::Bzip2 { .. } => CompressionAlgorithm::Bzip2,
111            CompressionSettings::Gzip { .. } => CompressionAlgorithm::Gzip,
112            CompressionSettings::Xz { .. } => CompressionAlgorithm::Xz,
113            CompressionSettings::Zstd { .. } => CompressionAlgorithm::Zstd,
114        }
115    }
116}
117
118/// A macro to define a compression level struct.
119///
120/// Accepts the `name` of the compression level struct, its `min`, `max` and `default` values, the
121/// `compression` executable it relates to and a `url`, that defines a man page for the
122/// `compression` executable.
123macro_rules! define_compression_level {
124    (
125        $name:ident,
126        Min => $min:expr,
127        Max => $max:expr,
128        Default => $default:expr,
129        $compression:literal,
130        $url:literal
131    ) => {
132        #[doc = concat!("Compression level for ", $compression, " compression.")]
133        #[derive(Clone, Debug, Eq, PartialEq)]
134        pub struct $name(u8);
135
136        impl $name {
137            #[doc = concat!("Creates a new [`", stringify!($name), "`] from a [`u8`].")]
138            ///
139            #[doc = concat!("The `level` must be in the range of [`", stringify!($name), "::min`] and [`", stringify!($name), "::max`].")]
140            ///
141            /// # Errors
142            ///
143            #[doc = concat!("Returns an error if the value is not in the range of [`", stringify!($name), "::min`] and [`", stringify!($name), "::max`].")]
144            pub fn new(level: u8) -> Result<Self, Error> {
145                trace!(concat!("Creating new compression level for ", $compression, " compression with {{level}}"));
146                if !($name::min()..=$name::max()).contains(&level) {
147                    return Err(Error::InvalidCompressionLevel {
148                        level,
149                        min: $name::min(),
150                        max: $name::max(),
151                    });
152                }
153                Ok(Self(level))
154            }
155
156            #[doc = concat!("Returns the default level (`", stringify!($default), "`) for [`", stringify!($name), "`].")]
157            ///
158            #[doc = concat!("The default level adheres to the one selected by the [", $compression, "] executable.")]
159            ///
160            #[doc = concat!("[", $compression, "]: ", $url)]
161            pub const fn default_level() -> u8 {
162                $default
163            }
164
165            #[doc = concat!("Returns the minimum allowed level (`", stringify!($min), "`) for [`", stringify!($name), "`].")]
166            pub const fn min() -> u8 {
167                $min
168            }
169
170            #[doc = concat!("Returns the maximum allowed level (`", stringify!($max), "`) for [`", stringify!($name), "`].")]
171            pub const fn max() -> u8 {
172                $max
173            }
174        }
175
176        impl Default for $name {
177            #[doc = concat!("Returns the default [`", stringify!($name), "`].")]
178            ///
179            #[doc = concat!("Delegates to [`", stringify!($name), "::default_level`] for retrieving the default compression level.")]
180            fn default() -> Self {
181                Self($name::default_level())
182            }
183        }
184
185        impl Display for $name {
186            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187                write!(f, "{}", self.0)
188            }
189        }
190
191        impl From<&$name> for i32 {
192            fn from(value: &$name) -> Self {
193                i32::from(value.0)
194            }
195        }
196
197        impl From<&$name> for u32 {
198            fn from(value: &$name) -> Self {
199                 u32::from(value.0)
200            }
201        }
202
203        impl TryFrom<i8> for $name {
204            type Error = Error;
205
206            fn try_from(value: i8) -> Result<Self, Error> {
207                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
208            }
209        }
210
211        impl TryFrom<i16> for $name {
212            type Error = Error;
213
214            fn try_from(value: i16) -> Result<Self, Error> {
215                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
216            }
217        }
218
219        impl TryFrom<i32> for $name {
220            type Error = Error;
221
222            fn try_from(value: i32) -> Result<Self, Error> {
223                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
224            }
225        }
226
227        impl TryFrom<i64> for $name {
228            type Error = Error;
229
230            fn try_from(value: i64) -> Result<Self, Error> {
231                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
232            }
233        }
234
235        impl TryFrom<u8> for $name {
236            type Error = Error;
237
238            fn try_from(value: u8) -> Result<Self, Error> {
239                 $name::new(value)
240            }
241        }
242
243        impl TryFrom<u16> for $name {
244            type Error = Error;
245
246            fn try_from(value: u16) -> Result<Self, Error> {
247                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
248            }
249        }
250
251        impl TryFrom<u32> for $name {
252            type Error = Error;
253
254            fn try_from(value: u32) -> Result<Self, Error> {
255                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
256            }
257        }
258
259        impl TryFrom<u64> for $name {
260            type Error = Error;
261
262            fn try_from(value: u64) -> Result<Self, Error> {
263                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
264            }
265        }
266    };
267}
268
269// Create the bzip2 compression level struct.
270define_compression_level!(
271    Bzip2CompressionLevel,
272    Min => 1,
273    Max => 9,
274    Default => 9,
275    "bzip2",
276    "https://man.archlinux.org/man/bzip2.1"
277);
278
279// Create the gzip compression level struct.
280define_compression_level!(
281    GzipCompressionLevel,
282    Min => 1,
283    Max => 9,
284    Default => 6,
285    "gzip",
286    "https://man.archlinux.org/man/gzip.1"
287);
288
289// Create the xz compression level struct.
290define_compression_level!(
291    XzCompressionLevel,
292    Min => 0,
293    Max => 9,
294    Default => 6,
295    "xz",
296    "https://man.archlinux.org/man/xz.1"
297);
298
299// Create the zstd compression level struct.
300define_compression_level!(
301    ZstdCompressionLevel,
302    Min => 0,
303    Max => 22,
304    Default => 3,
305    "zstd",
306    "https://man.archlinux.org/man/zstd.1"
307);
308
309/// The amount of threads to use when compressing using zstd.
310///
311/// The default (1) adheres to the one selected by the [zstd] executable.
312/// If the custom amount of `0` is used, all available physical CPU cores are used.
313///
314/// [zstd]: https://man.archlinux.org/man/zstd.1
315#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
316pub struct ZstdThreads(u32);
317
318impl ZstdThreads {
319    /// Creates a new [`ZstdThreads`] from a [`u32`].
320    pub fn new(threads: u32) -> Self {
321        Self(threads)
322    }
323
324    /// Creates a new [`ZstdThreads`] which uses all physical CPU cores.
325    ///
326    /// This is short for calling [`ZstdThreads::new`] with `0`.
327    pub fn all() -> Self {
328        Self(0)
329    }
330}
331
332impl Default for ZstdThreads {
333    /// Returns the default thread value (1) when compressing with zstd.
334    ///
335    /// The default adheres to the one selected by the [zstd] executable.
336    ///
337    /// [zstd]: https://man.archlinux.org/man/zstd.1
338    fn default() -> Self {
339        Self(1)
340    }
341}
342
343/// Settings for a compression encoder.
344#[derive(Clone, Debug, Eq, PartialEq)]
345pub enum CompressionSettings {
346    /// Settings for the bzip2 compression algorithm.
347    Bzip2 {
348        /// The used compression level.
349        compression_level: Bzip2CompressionLevel,
350    },
351
352    /// Settings for the gzip compression algorithm.
353    Gzip {
354        /// The used compression level.
355        compression_level: GzipCompressionLevel,
356    },
357
358    /// Settings for the xz compression algorithm.
359    Xz {
360        /// The used compression level.
361        compression_level: XzCompressionLevel,
362    },
363
364    /// Settings for the zstandard compression algorithm.
365    Zstd {
366        /// The used compression level.
367        compression_level: ZstdCompressionLevel,
368        /// The amount of threads to use when compressing.
369        threads: ZstdThreads,
370    },
371}
372
373impl Default for CompressionSettings {
374    /// Returns [`CompressionSettings::Zstd`].
375    ///
376    /// Defaults for `compression_level` and `threads` follow that of the [zstd] executable.
377    ///
378    /// [zstd]: https://man.archlinux.org/man/zstd.1
379    fn default() -> Self {
380        Self::Zstd {
381            compression_level: ZstdCompressionLevel::default(),
382            threads: ZstdThreads::default(),
383        }
384    }
385}
386
387impl From<&CompressionSettings> for CompressionAlgorithmFileExtension {
388    /// Creates a [`CompressionAlgorithmFileExtension`] from a [`CompressionSettings`].
389    fn from(value: &CompressionSettings) -> Self {
390        match value {
391            CompressionSettings::Bzip2 { .. } => CompressionAlgorithmFileExtension::Bzip2,
392            CompressionSettings::Gzip { .. } => CompressionAlgorithmFileExtension::Gzip,
393            CompressionSettings::Xz { .. } => CompressionAlgorithmFileExtension::Xz,
394            CompressionSettings::Zstd { .. } => CompressionAlgorithmFileExtension::Zstd,
395        }
396    }
397}
398
399/// Creates and configures an [`Encoder`].
400///
401/// Uses a dedicated `compression_level` and amount of `threads` to construct and configure an
402/// encoder for zstd compression.
403/// The `settings` are merely used for additional context in cases of error.
404///
405/// # Errors
406///
407/// Returns an error if
408///
409/// - the encoder cannot be created using the `file` and `compression_level`,
410/// - the encoder cannot be configured to use checksums at the end of each frame,
411/// - the amount of physical CPU cores can not be turned into a `u32`,
412/// - or multithreading can not be enabled based on the provided `threads` settings.
413fn create_zstd_encoder(
414    file: File,
415    compression_level: &ZstdCompressionLevel,
416    threads: &ZstdThreads,
417    settings: &CompressionSettings,
418) -> Result<Encoder<'static, File>, Error> {
419    let mut encoder = Encoder::new(file, compression_level.into()).map_err(|source| {
420        Error::CreateZstandardEncoder {
421            context: "initializing",
422            compression_settings: settings.clone(),
423            source,
424        }
425    })?;
426    // Include a context checksum at the end of each frame.
427    encoder
428        .include_checksum(true)
429        .map_err(|source| Error::CreateZstandardEncoder {
430            context: "setting checksums to be added",
431            compression_settings: settings.clone(),
432            source,
433        })?;
434
435    // Get amount of threads to use.
436    let threads = match threads {
437        // Use available physical CPU cores if the special value `0` is used.
438        // NOTE: For the zstd executable `0` means "use all available threads", while for the zstd
439        // crate this means "disable multithreading".
440        ZstdThreads(0) => {
441            u32::try_from(num_cpus::get_physical()).map_err(Error::IntegerConversion)?
442        }
443        ZstdThreads(threads) => *threads,
444    };
445
446    // Use multi-threading if it is available.
447    encoder
448        .multithread(threads)
449        .map_err(|source| Error::CreateZstandardEncoder {
450            context: "setting checksums to be added",
451            compression_settings: settings.clone(),
452            source,
453        })?;
454
455    Ok(encoder)
456}
457
458/// Encoder for compression which supports multiple backends.
459///
460/// Wraps [`BzEncoder`], [`GzEncoder`], [`XzEncoder`] and [`Encoder`].
461/// Provides a unified [`Write`] implementation across all of them.
462pub enum CompressionEncoder<'a> {
463    /// The bzip2 compression encoder.
464    Bzip2(BzEncoder<File>),
465
466    /// The gzip compression encoder.
467    Gzip(GzEncoder<File>),
468
469    /// The xz compression encoder.
470    Xz(XzEncoder<File>),
471
472    /// The zstd compression encoder.
473    Zstd(Encoder<'a, File>),
474}
475
476impl CompressionEncoder<'_> {
477    /// Creates a new [`CompressionEncoder`].
478    ///
479    /// Uses a [`File`] to stream to and initializes a specific backend based on the provided
480    /// [`CompressionSettings`].
481    ///
482    /// # Errors
483    ///
484    /// Returns an error if creating the encoder for zstd compression fails.
485    /// All other encoder initializations are infallible.
486    pub fn new(file: File, settings: &CompressionSettings) -> Result<Self, Error> {
487        Ok(match settings {
488            CompressionSettings::Bzip2 { compression_level } => Self::Bzip2(BzEncoder::new(
489                file,
490                bzip2::Compression::new(compression_level.into()),
491            )),
492            CompressionSettings::Gzip { compression_level } => Self::Gzip(GzEncoder::new(
493                file,
494                flate2::Compression::new(compression_level.into()),
495            )),
496            CompressionSettings::Xz { compression_level } => {
497                Self::Xz(XzEncoder::new_parallel(file, compression_level.into()))
498            }
499            CompressionSettings::Zstd {
500                compression_level,
501                threads,
502            } => Self::Zstd(create_zstd_encoder(
503                file,
504                compression_level,
505                threads,
506                settings,
507            )?),
508        })
509    }
510
511    /// Finishes the compression stream.
512    ///
513    /// # Error
514    ///
515    /// Returns an error if the wrapped encoder fails.
516    pub fn finish(self) -> Result<File, Error> {
517        match self {
518            CompressionEncoder::Bzip2(encoder) => {
519                encoder.finish().map_err(|source| Error::FinishEncoder {
520                    compression_type: CompressionAlgorithmFileExtension::Bzip2,
521                    source,
522                })
523            }
524            CompressionEncoder::Gzip(encoder) => {
525                encoder.finish().map_err(|source| Error::FinishEncoder {
526                    compression_type: CompressionAlgorithmFileExtension::Gzip,
527                    source,
528                })
529            }
530            CompressionEncoder::Xz(encoder) => {
531                encoder.finish().map_err(|source| Error::FinishEncoder {
532                    compression_type: CompressionAlgorithmFileExtension::Xz,
533                    source,
534                })
535            }
536            CompressionEncoder::Zstd(encoder) => {
537                encoder.finish().map_err(|source| Error::FinishEncoder {
538                    compression_type: CompressionAlgorithmFileExtension::Zstd,
539                    source,
540                })
541            }
542        }
543    }
544}
545
546impl Debug for CompressionEncoder<'_> {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        write!(
549            f,
550            "CompressionEncoder({})",
551            match self {
552                CompressionEncoder::Bzip2(_) => "Bzip2",
553                CompressionEncoder::Gzip(_) => "Gzip",
554                CompressionEncoder::Xz(_) => "Xz",
555                CompressionEncoder::Zstd(_) => "Zstd",
556            }
557        )
558    }
559}
560
561impl Write for CompressionEncoder<'_> {
562    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
563        match self {
564            CompressionEncoder::Bzip2(encoder) => encoder.write(buf),
565            CompressionEncoder::Gzip(encoder) => encoder.write(buf),
566            CompressionEncoder::Xz(encoder) => encoder.write(buf),
567            CompressionEncoder::Zstd(encoder) => encoder.write(buf),
568        }
569    }
570
571    fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
572        match self {
573            CompressionEncoder::Bzip2(encoder) => encoder.write_vectored(bufs),
574            CompressionEncoder::Gzip(encoder) => encoder.write_vectored(bufs),
575            CompressionEncoder::Xz(encoder) => encoder.write_vectored(bufs),
576            CompressionEncoder::Zstd(encoder) => encoder.write_vectored(bufs),
577        }
578    }
579
580    fn flush(&mut self) -> std::io::Result<()> {
581        match self {
582            CompressionEncoder::Bzip2(encoder) => encoder.flush(),
583            CompressionEncoder::Gzip(encoder) => encoder.flush(),
584            CompressionEncoder::Xz(encoder) => encoder.flush(),
585            CompressionEncoder::Zstd(encoder) => encoder.flush(),
586        }
587    }
588
589    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
590        match self {
591            CompressionEncoder::Bzip2(encoder) => encoder.write_all(buf),
592            CompressionEncoder::Gzip(encoder) => encoder.write_all(buf),
593            CompressionEncoder::Xz(encoder) => encoder.write_all(buf),
594            CompressionEncoder::Zstd(encoder) => encoder.write_all(buf),
595        }
596    }
597
598    fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> {
599        match self {
600            CompressionEncoder::Bzip2(encoder) => encoder.write_fmt(fmt),
601            CompressionEncoder::Gzip(encoder) => encoder.write_fmt(fmt),
602            CompressionEncoder::Xz(encoder) => encoder.write_fmt(fmt),
603            CompressionEncoder::Zstd(encoder) => encoder.write_fmt(fmt),
604        }
605    }
606
607    fn by_ref(&mut self) -> &mut Self
608    where
609        Self: Sized,
610    {
611        self
612    }
613}
614
615/// Decoder for decompression which supports multiple backends.
616///
617/// Wraps [`BzDecoder`], [`GzDecoder`], [`XzDecoder`] and [`Decoder`]
618/// and provides a unified [`Read`] implementation across all of them.
619pub enum CompressionDecoder<'a> {
620    /// The bzip2 decompression decoder.
621    Bzip2(BzDecoder<BufReader<File>>),
622
623    /// The gzip decompression decoder.
624    Gzip(GzDecoder<BufReader<File>>),
625
626    /// The xz decompression decoder.
627    Xz(XzDecoder<BufReader<File>>),
628
629    /// The zstd decompression decoder.
630    Zstd(Decoder<'a, BufReader<File>>),
631}
632
633impl CompressionDecoder<'_> {
634    /// Creates a new [`CompressionDecoder`].
635    ///
636    /// Uses a [`File`] to stream from and initializes a specific backend based on the provided
637    /// [`CompressionAlgorithm`].
638    ///
639    /// # Errors
640    ///
641    /// Returns an error if creating the decoder for zstd compression fails
642    /// (all other decoder initializations are infallible).
643    pub fn new(file: File, algorithm: CompressionAlgorithm) -> Result<Self, Error> {
644        match algorithm {
645            CompressionAlgorithm::Bzip2 => Ok(Self::Bzip2(BzDecoder::new(BufReader::new(file)))),
646            CompressionAlgorithm::Gzip => Ok(Self::Gzip(GzDecoder::new(BufReader::new(file)))),
647            CompressionAlgorithm::Xz => Ok(Self::Xz(XzDecoder::new(BufReader::new(file)))),
648            CompressionAlgorithm::Zstd => Ok(Self::Zstd(
649                Decoder::new(file).map_err(Error::CreateZstandardDecoder)?,
650            )),
651        }
652    }
653}
654
655impl Debug for CompressionDecoder<'_> {
656    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657        write!(
658            f,
659            "CompressionDecoder({})",
660            match self {
661                CompressionDecoder::Bzip2(_) => "Bzip2",
662                CompressionDecoder::Gzip(_) => "Gzip",
663                CompressionDecoder::Xz(_) => "Xz",
664                CompressionDecoder::Zstd(_) => "Zstd",
665            }
666        )
667    }
668}
669
670impl Read for CompressionDecoder<'_> {
671    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
672        match self {
673            CompressionDecoder::Bzip2(decoder) => decoder.read(buf),
674            CompressionDecoder::Gzip(decoder) => decoder.read(buf),
675            CompressionDecoder::Xz(decoder) => decoder.read(buf),
676            CompressionDecoder::Zstd(decoder) => decoder.read(buf),
677        }
678    }
679
680    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
681        match self {
682            CompressionDecoder::Bzip2(decoder) => decoder.read_to_end(buf),
683            CompressionDecoder::Gzip(decoder) => decoder.read_to_end(buf),
684            CompressionDecoder::Xz(decoder) => decoder.read_to_end(buf),
685            CompressionDecoder::Zstd(decoder) => decoder.read_to_end(buf),
686        }
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use std::io::{IoSlice, Seek};
693
694    use proptest::{proptest, test_runner::Config as ProptestConfig};
695    use rstest::rstest;
696    use tempfile::tempfile;
697    use testresult::TestResult;
698
699    use super::*;
700
701    proptest! {
702        #![proptest_config(ProptestConfig::with_cases(1000))]
703
704        #[test]
705        fn valid_bzip2_compression_level_try_from_i8(input in 1..=9i8) {
706            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
707        }
708
709        #[test]
710        fn valid_bzip2_compression_level_try_from_i16(input in 1..=9i16) {
711            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
712        }
713
714        #[test]
715        fn valid_bzip2_compression_level_try_from_i32(input in 1..=9i32) {
716            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
717        }
718
719        #[test]
720        fn valid_bzip2_compression_level_try_from_i64(input in 1..=9i64) {
721            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
722        }
723
724        #[test]
725        fn valid_bzip2_compression_level_try_from_u8(input in 1..=9u8) {
726            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
727        }
728
729        #[test]
730        fn valid_bzip2_compression_level_try_from_u16(input in 1..=9u16) {
731            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
732        }
733
734        #[test]
735        fn valid_bzip2_compression_level_try_from_u32(input in 1..=9u32) {
736            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
737        }
738
739        #[test]
740        fn valid_bzip2_compression_level_try_from_u64(input in 1..=9u64) {
741            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
742        }
743
744        #[test]
745        fn valid_gzip_compression_level_try_from_i8(input in 1..=9i8) {
746            assert!(GzipCompressionLevel::try_from(input).is_ok());
747        }
748
749        #[test]
750        fn valid_gzip_compression_level_try_from_i16(input in 1..=9i16) {
751            assert!(GzipCompressionLevel::try_from(input).is_ok());
752        }
753
754        #[test]
755        fn valid_gzip_compression_level_try_from_i32(input in 1..=9i32) {
756            assert!(GzipCompressionLevel::try_from(input).is_ok());
757        }
758
759        #[test]
760        fn valid_gzip_compression_level_try_from_i64(input in 1..=9i64) {
761            assert!(GzipCompressionLevel::try_from(input).is_ok());
762        }
763
764        #[test]
765        fn valid_gzip_compression_level_try_from_u8(input in 1..=9u8) {
766            assert!(GzipCompressionLevel::try_from(input).is_ok());
767        }
768
769        #[test]
770        fn valid_gzip_compression_level_try_from_u16(input in 1..=9u16) {
771            assert!(GzipCompressionLevel::try_from(input).is_ok());
772        }
773
774        #[test]
775        fn valid_gzip_compression_level_try_from_u32(input in 1..=9u32) {
776            assert!(GzipCompressionLevel::try_from(input).is_ok());
777        }
778
779        #[test]
780        fn valid_gzip_compression_level_try_from_u64(input in 1..=9u64) {
781            assert!(GzipCompressionLevel::try_from(input).is_ok());
782        }
783
784        #[test]
785        fn valid_xz_compression_level_try_from_i8(input in 0..=9i8) {
786            assert!(XzCompressionLevel::try_from(input).is_ok());
787        }
788
789        #[test]
790        fn valid_xz_compression_level_try_from_i16(input in 0..=9i16) {
791            assert!(XzCompressionLevel::try_from(input).is_ok());
792        }
793
794        #[test]
795        fn valid_xz_compression_level_try_from_i32(input in 0..=9i32) {
796            assert!(XzCompressionLevel::try_from(input).is_ok());
797        }
798
799        #[test]
800        fn valid_xz_compression_level_try_from_i64(input in 0..=9i64) {
801            assert!(XzCompressionLevel::try_from(input).is_ok());
802        }
803
804        #[test]
805        fn valid_xz_compression_level_try_from_u8(input in 0..=9u8) {
806            assert!(XzCompressionLevel::try_from(input).is_ok());
807        }
808
809        #[test]
810        fn valid_xz_compression_level_try_from_u16(input in 0..=9u16) {
811            assert!(XzCompressionLevel::try_from(input).is_ok());
812        }
813
814        #[test]
815        fn valid_xz_compression_level_try_from_u32(input in 0..=9u32) {
816            assert!(XzCompressionLevel::try_from(input).is_ok());
817        }
818
819        #[test]
820        fn valid_xz_compression_level_try_from_u64(input in 0..=9u64) {
821            assert!(XzCompressionLevel::try_from(input).is_ok());
822        }
823
824        #[test]
825        fn valid_zstd_compression_level_try_from_i8(input in 0..=22i8) {
826            assert!(ZstdCompressionLevel::try_from(input).is_ok());
827        }
828
829        #[test]
830        fn valid_zstd_compression_level_try_from_i16(input in 0..=22i16) {
831            assert!(ZstdCompressionLevel::try_from(input).is_ok());
832        }
833
834        #[test]
835        fn valid_zstd_compression_level_try_from_i32(input in 0..=22i32) {
836            assert!(ZstdCompressionLevel::try_from(input).is_ok());
837        }
838
839        #[test]
840        fn valid_zstd_compression_level_try_from_i64(input in 0..=22i64) {
841            assert!(ZstdCompressionLevel::try_from(input).is_ok());
842        }
843
844        #[test]
845        fn valid_zstd_compression_level_try_from_u8(input in 0..=22u8) {
846            assert!(ZstdCompressionLevel::try_from(input).is_ok());
847        }
848
849        #[test]
850        fn valid_zstd_compression_level_try_from_u16(input in 0..=22u16) {
851            assert!(ZstdCompressionLevel::try_from(input).is_ok());
852        }
853
854        #[test]
855        fn valid_zstd_compression_level_try_from_u32(input in 0..=22u32) {
856            assert!(ZstdCompressionLevel::try_from(input).is_ok());
857        }
858
859        #[test]
860        fn valid_zstd_compression_level_try_from_u64(input in 0..=22u64) {
861            assert!(ZstdCompressionLevel::try_from(input).is_ok());
862        }
863    }
864
865    #[rstest]
866    #[case::too_large(Bzip2CompressionLevel::max() + 1)]
867    #[case::too_small(Bzip2CompressionLevel::min() - 1)]
868    fn create_bzip2_compression_level_fails(#[case] level: u8) -> TestResult {
869        if let Ok(level) = Bzip2CompressionLevel::new(level) {
870            return Err(format!("Should not have succeeded but created level: {level}").into());
871        }
872
873        Ok(())
874    }
875
876    #[test]
877    fn create_bzip2_compression_level_succeeds() -> TestResult {
878        if let Err(error) = Bzip2CompressionLevel::new(6) {
879            return Err(format!("Should have succeeded but raised error:\n{error}").into());
880        }
881
882        Ok(())
883    }
884
885    #[rstest]
886    #[case::too_large(GzipCompressionLevel::max() + 1)]
887    #[case::too_small(GzipCompressionLevel::min() - 1)]
888    fn create_gzip_compression_level_fails(#[case] level: u8) -> TestResult {
889        if let Ok(level) = GzipCompressionLevel::new(level) {
890            return Err(format!("Should not have succeeded but created level: {level}").into());
891        }
892
893        Ok(())
894    }
895
896    #[test]
897    fn create_gzip_compression_level_succeeds() -> TestResult {
898        if let Err(error) = GzipCompressionLevel::new(6) {
899            return Err(format!("Should have succeeded but raised error:\n{error}").into());
900        }
901
902        Ok(())
903    }
904
905    #[test]
906    fn create_xz_compression_level_fails() -> TestResult {
907        if let Ok(level) = XzCompressionLevel::new(XzCompressionLevel::max() + 1) {
908            return Err(format!("Should not have succeeded but created level: {level}").into());
909        }
910
911        Ok(())
912    }
913
914    #[test]
915    fn create_xz_compression_level_succeeds() -> TestResult {
916        if let Err(error) = XzCompressionLevel::new(6) {
917            return Err(format!("Should have succeeded but raised error:\n{error}").into());
918        }
919
920        Ok(())
921    }
922
923    #[test]
924    fn create_zstd_compression_level_fails() -> TestResult {
925        if let Ok(level) = ZstdCompressionLevel::new(ZstdCompressionLevel::max() + 1) {
926            return Err(format!("Should not have succeeded but created level: {level}").into());
927        }
928
929        Ok(())
930    }
931
932    #[test]
933    fn create_zstd_compression_level_succeeds() -> TestResult {
934        if let Err(error) = ZstdCompressionLevel::new(6) {
935            return Err(format!("Should have succeeded but raised error:\n{error}").into());
936        }
937
938        Ok(())
939    }
940
941    /// Ensures that the default [`CompressionSettings`] are those for zstd.
942    #[test]
943    fn default_compression_settings() -> TestResult {
944        assert!(matches!(
945            CompressionSettings::default(),
946            CompressionSettings::Zstd {
947                compression_level: _,
948                threads: _,
949            }
950        ));
951        Ok(())
952    }
953
954    /// Ensures that the [`Write::write`] implementation works for each [`CompressionEncoder`].
955    #[rstest]
956    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
957    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
958    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
959    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
960    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
961    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
962    fn test_compression_encoder_write(#[case] settings: CompressionSettings) -> TestResult {
963        let file = tempfile()?;
964        let mut encoder = CompressionEncoder::new(file, &settings)?;
965        let ref_encoder = encoder.by_ref();
966        let buf = &[1; 8];
967
968        let mut write_len = 0;
969        while write_len < buf.len() {
970            let len_written = ref_encoder.write(buf)?;
971            write_len += len_written;
972        }
973
974        ref_encoder.flush()?;
975
976        Ok(())
977    }
978
979    /// Ensures that the [`Write::write_vectored`] implementation works for each
980    /// [`CompressionEncoder`].
981    #[rstest]
982    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
983    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
984    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
985    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
986    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
987    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
988    fn test_compression_encoder_write_vectored(
989        #[case] settings: CompressionSettings,
990    ) -> TestResult {
991        let file = tempfile()?;
992        let mut encoder = CompressionEncoder::new(file, &settings)?;
993        let ref_encoder = encoder.by_ref();
994
995        let data1 = [1; 8];
996        let data2 = [15; 8];
997        let io_slice1 = IoSlice::new(&data1);
998        let io_slice2 = IoSlice::new(&data2);
999
1000        let mut write_len = 0;
1001        while write_len < data1.len() + data2.len() {
1002            let len_written = ref_encoder.write_vectored(&[io_slice1, io_slice2])?;
1003            write_len += len_written;
1004        }
1005
1006        ref_encoder.flush()?;
1007
1008        Ok(())
1009    }
1010
1011    /// Ensures that the [`Write::write_all`] implementation works for each [`CompressionEncoder`].
1012    #[rstest]
1013    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
1014    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
1015    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
1016    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
1017    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
1018    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
1019    fn test_compression_encoder_write_all(#[case] settings: CompressionSettings) -> TestResult {
1020        let file = tempfile()?;
1021        let mut encoder = CompressionEncoder::new(file, &settings)?;
1022        let ref_encoder = encoder.by_ref();
1023        let buf = &[1; 8];
1024
1025        ref_encoder.write_all(buf)?;
1026
1027        ref_encoder.flush()?;
1028
1029        Ok(())
1030    }
1031
1032    /// Ensures that the [`Write::write_fmt`] implementation works for each [`CompressionEncoder`].
1033    #[rstest]
1034    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
1035    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
1036    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
1037    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
1038    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
1039    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
1040    fn test_compression_encoder_write_fmt(#[case] settings: CompressionSettings) -> TestResult {
1041        let file = tempfile()?;
1042        let mut encoder = CompressionEncoder::new(file, &settings)?;
1043        let ref_encoder = encoder.by_ref();
1044
1045        ref_encoder.write_fmt(format_args!("{:.*}", 2, 1.234567))?;
1046
1047        ref_encoder.flush()?;
1048
1049        Ok(())
1050    }
1051
1052    /// Ensures that the [`CompressionDecoder`] can decompress data compressed by
1053    /// [`CompressionEncoder`].
1054    #[rstest]
1055    #[case::bzip2(CompressionAlgorithm::Bzip2, CompressionSettings::Bzip2 {
1056        compression_level: Bzip2CompressionLevel::default()
1057    })]
1058    #[case::gzip(CompressionAlgorithm::Gzip, CompressionSettings::Gzip {
1059        compression_level: GzipCompressionLevel::default()
1060    })]
1061    #[case::xz(CompressionAlgorithm::Xz, CompressionSettings::Xz {
1062        compression_level: XzCompressionLevel::default()
1063    })]
1064    #[case::zstd(CompressionAlgorithm::Zstd, CompressionSettings::Zstd {
1065        compression_level: ZstdCompressionLevel::default(),
1066        threads: ZstdThreads::new(0),
1067    })]
1068    fn test_compression_decoder_roundtrip(
1069        #[case] algorithm: CompressionAlgorithm,
1070        #[case] settings: CompressionSettings,
1071    ) -> TestResult {
1072        // Prepare some sample data
1073        let input_data = b"alpm4ever";
1074
1075        // Compress it
1076        let mut file = tempfile()?;
1077        {
1078            let mut encoder = CompressionEncoder::new(file.try_clone()?, &settings)?;
1079            encoder.write_all(input_data)?;
1080            encoder.flush()?;
1081            encoder.finish()?;
1082        }
1083
1084        // Rewind the file
1085        file.rewind()?;
1086
1087        // Decompress it
1088        let mut decoder = CompressionDecoder::new(file, algorithm)?;
1089        let mut output = Vec::new();
1090        decoder.read_to_end(&mut output)?;
1091
1092        // Check data integrity
1093        assert_eq!(output, input_data);
1094        Ok(())
1095    }
1096
1097    /// Ensures that the conversion from [`CompressionAlgorithmFileExtension`] to
1098    /// [`CompressionAlgorithm`] works as expected.
1099    #[rstest]
1100    #[case(CompressionAlgorithmFileExtension::Bzip2, CompressionAlgorithm::Bzip2)]
1101    #[case(CompressionAlgorithmFileExtension::Gzip, CompressionAlgorithm::Gzip)]
1102    #[case(CompressionAlgorithmFileExtension::Xz, CompressionAlgorithm::Xz)]
1103    #[case(CompressionAlgorithmFileExtension::Zstd, CompressionAlgorithm::Zstd)]
1104    fn test_compression_algorithm_conversion_success(
1105        #[case] ext: CompressionAlgorithmFileExtension,
1106        #[case] expected: CompressionAlgorithm,
1107    ) -> TestResult {
1108        let result = CompressionAlgorithm::try_from(ext)?;
1109        assert_eq!(result, expected);
1110        Ok(())
1111    }
1112
1113    /// Ensures that the conversion from [`CompressionAlgorithmFileExtension`] to
1114    /// [`CompressionAlgorithm`] fails as expected for unsupported algorithms.
1115    #[rstest]
1116    #[case(CompressionAlgorithmFileExtension::Compress, "Z")]
1117    #[case(CompressionAlgorithmFileExtension::Lrzip, "lrz")]
1118    #[case(CompressionAlgorithmFileExtension::Lzip, "lz")]
1119    #[case(CompressionAlgorithmFileExtension::Lz4, "lz4")]
1120    #[case(CompressionAlgorithmFileExtension::Lzop, "lzo")]
1121    fn test_compression_algorithm_conversion_failure(
1122        #[case] ext: CompressionAlgorithmFileExtension,
1123        #[case] expected_str: &str,
1124    ) -> TestResult {
1125        match CompressionAlgorithm::try_from(ext) {
1126            Ok(algorithm) => Err(format!("Expected failure, but got: {algorithm:?}").into()),
1127            Err(Error::UnsupportedCompressionAlgorithm { value }) => {
1128                assert_eq!(value, expected_str);
1129                Ok(())
1130            }
1131            Err(e) => Err(format!("Unexpected error variant: {e:?}").into()),
1132        }
1133    }
1134
1135    /// Ensures that the conversion from [`CompressionSettings`] to
1136    /// [`CompressionAlgorithm`] works as expected.
1137    #[rstest]
1138    #[case::bzip2(CompressionSettings::Bzip2 {
1139        compression_level: Bzip2CompressionLevel::default()
1140    }, CompressionAlgorithm::Bzip2)]
1141    #[case::gzip(CompressionSettings::Gzip {
1142        compression_level: GzipCompressionLevel::default()
1143    }, CompressionAlgorithm::Gzip)]
1144    #[case::xz(CompressionSettings::Xz {
1145        compression_level: XzCompressionLevel::default()
1146    }, CompressionAlgorithm::Xz)]
1147    #[case::zstd(CompressionSettings::Zstd {
1148        compression_level: ZstdCompressionLevel::default(),
1149        threads: ZstdThreads::new(4),
1150    }, CompressionAlgorithm::Zstd)]
1151    fn test_from_compression_settings_to_algorithm(
1152        #[case] settings: CompressionSettings,
1153        #[case] expected: CompressionAlgorithm,
1154    ) -> TestResult {
1155        let result = CompressionAlgorithm::from(&settings);
1156        assert_eq!(result, expected);
1157        Ok(())
1158    }
1159
1160    #[rstest]
1161    #[case::bzip2(CompressionAlgorithm::Bzip2)]
1162    #[case::gzip(CompressionAlgorithm::Gzip)]
1163    #[case::xz(CompressionAlgorithm::Xz)]
1164    #[case::zstd(CompressionAlgorithm::Zstd)]
1165    fn test_compression_decoder_debug(#[case] algorithm: CompressionAlgorithm) -> TestResult {
1166        let file = tempfile()?;
1167        let decoder = CompressionDecoder::new(file, algorithm)?;
1168        let debug_str = format!("{decoder:?}");
1169        assert!(debug_str.contains("CompressionDecoder"));
1170        Ok(())
1171    }
1172}