alpm_package/
compression.rs

1//! Compression handling.
2
3use std::{
4    fmt::{Debug, Display},
5    fs::File,
6    io::Write,
7    num::TryFromIntError,
8};
9
10use alpm_types::CompressionAlgorithmFileExtension;
11use bzip2::write::BzEncoder;
12use flate2::write::GzEncoder;
13use liblzma::write::XzEncoder;
14use log::trace;
15use zstd::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 finishing a compression encoder.
37    #[error("Error while finishing {compression_type} compression encoder:\n{source}")]
38    FinishEncoder {
39        /// The compression chosen for the encoder.
40        compression_type: CompressionAlgorithmFileExtension,
41        /// The source error
42        source: std::io::Error,
43    },
44
45    /// An error occurred while trying to get the available parallelism.
46    #[error("Error while trying to get available parallelism:\n{0}")]
47    GetParallelism(#[source] std::io::Error),
48
49    /// An error occurred while trying to convert an integer.
50    #[error("Error while trying to convert an integer:\n{0}")]
51    IntegerConversion(#[source] TryFromIntError),
52
53    /// A compression level is not valid.
54    #[error("Invalid compression level {level} (must be in the range {min} - {max})")]
55    InvalidCompressionLevel {
56        /// The invalid compression level.
57        level: u8,
58        /// The minimum valid compression level.
59        min: u8,
60        /// The maximum valid compression level.
61        max: u8,
62    },
63}
64
65/// A macro to define a compression level struct.
66///
67/// Accepts the `name` of the compression level struct, its `min`, `max` and `default` values, the
68/// `compression` executable it relates to and a `url`, that defines a man page for the
69/// `compression` executable.
70macro_rules! define_compression_level {
71    (
72        $name:ident,
73        Min => $min:expr,
74        Max => $max:expr,
75        Default => $default:expr,
76        $compression:literal,
77        $url:literal
78    ) => {
79        #[doc = concat!("Compression level for ", $compression, " compression.")]
80        #[derive(Clone, Debug, Eq, PartialEq)]
81        pub struct $name(u8);
82
83        impl $name {
84            #[doc = concat!("Creates a new [`", stringify!($name), "`] from a [`u8`].")]
85            ///
86            #[doc = concat!("The `level` must be in the range of [`", stringify!($name), "::min`] and [`", stringify!($name), "::max`].")]
87            ///
88            /// # Errors
89            ///
90            #[doc = concat!("Returns an error if the value is not in the range of [`", stringify!($name), "::min`] and [`", stringify!($name), "::max`].")]
91            pub fn new(level: u8) -> Result<Self, Error> {
92                trace!(concat!("Creating new compression level for ", $compression, " compression with {{level}}"));
93                if !($name::min()..=$name::max()).contains(&level) {
94                    return Err(Error::InvalidCompressionLevel {
95                        level,
96                        min: $name::min(),
97                        max: $name::max(),
98                    });
99                }
100                Ok(Self(level))
101            }
102
103            #[doc = concat!("Returns the default level (`", stringify!($default), "`) for [`", stringify!($name), "`].")]
104            ///
105            #[doc = concat!("The default level adheres to the one selected by the [", $compression, "] executable.")]
106            ///
107            #[doc = concat!("[", $compression, "]: ", $url)]
108            pub const fn default_level() -> u8 {
109                $default
110            }
111
112            #[doc = concat!("Returns the minimum allowed level (`", stringify!($min), "`) for [`", stringify!($name), "`].")]
113            pub const fn min() -> u8 {
114                $min
115            }
116
117            #[doc = concat!("Returns the maximum allowed level (`", stringify!($max), "`) for [`", stringify!($name), "`].")]
118            pub const fn max() -> u8 {
119                $max
120            }
121        }
122
123        impl Default for $name {
124            #[doc = concat!("Returns the default [`", stringify!($name), "`].")]
125            ///
126            #[doc = concat!("Delegates to [`", stringify!($name), "::default_level`] for retrieving the default compression level.")]
127            fn default() -> Self {
128                Self($name::default_level())
129            }
130        }
131
132        impl Display for $name {
133            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134                write!(f, "{}", self.0)
135            }
136        }
137
138        impl From<&$name> for i32 {
139            fn from(value: &$name) -> Self {
140                i32::from(value.0)
141            }
142        }
143
144        impl From<&$name> for u32 {
145            fn from(value: &$name) -> Self {
146                 u32::from(value.0)
147            }
148        }
149
150        impl TryFrom<i8> for $name {
151            type Error = Error;
152
153            fn try_from(value: i8) -> Result<Self, Error> {
154                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
155            }
156        }
157
158        impl TryFrom<i16> for $name {
159            type Error = Error;
160
161            fn try_from(value: i16) -> Result<Self, Error> {
162                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
163            }
164        }
165
166        impl TryFrom<i32> for $name {
167            type Error = Error;
168
169            fn try_from(value: i32) -> Result<Self, Error> {
170                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
171            }
172        }
173
174        impl TryFrom<i64> for $name {
175            type Error = Error;
176
177            fn try_from(value: i64) -> Result<Self, Error> {
178                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
179            }
180        }
181
182        impl TryFrom<u8> for $name {
183            type Error = Error;
184
185            fn try_from(value: u8) -> Result<Self, Error> {
186                 $name::new(value)
187            }
188        }
189
190        impl TryFrom<u16> for $name {
191            type Error = Error;
192
193            fn try_from(value: u16) -> Result<Self, Error> {
194                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
195            }
196        }
197
198        impl TryFrom<u32> for $name {
199            type Error = Error;
200
201            fn try_from(value: u32) -> Result<Self, Error> {
202                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
203            }
204        }
205
206        impl TryFrom<u64> for $name {
207            type Error = Error;
208
209            fn try_from(value: u64) -> Result<Self, Error> {
210                 $name::new(u8::try_from(value).map_err(Error::IntegerConversion)?)
211            }
212        }
213    };
214}
215
216// Create the bzip2 compression level struct.
217define_compression_level!(
218    Bzip2CompressionLevel,
219    Min => 1,
220    Max => 9,
221    Default => 9,
222    "bzip2",
223    "https://man.archlinux.org/man/bzip2.1"
224);
225
226// Create the gzip compression level struct.
227define_compression_level!(
228    GzipCompressionLevel,
229    Min => 1,
230    Max => 9,
231    Default => 6,
232    "gzip",
233    "https://man.archlinux.org/man/gzip.1"
234);
235
236// Create the xz compression level struct.
237define_compression_level!(
238    XzCompressionLevel,
239    Min => 0,
240    Max => 9,
241    Default => 6,
242    "xz",
243    "https://man.archlinux.org/man/xz.1"
244);
245
246// Create the zstd compression level struct.
247define_compression_level!(
248    ZstdCompressionLevel,
249    Min => 0,
250    Max => 22,
251    Default => 3,
252    "zstd",
253    "https://man.archlinux.org/man/zstd.1"
254);
255
256/// The amount of threads to use when compressing using zstd.
257///
258/// The default (1) adheres to the one selected by the [zstd] executable.
259/// If the custom amount of `0` is used, all available physical CPU cores are used.
260///
261/// [zstd]: https://man.archlinux.org/man/zstd.1
262#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
263pub struct ZstdThreads(u32);
264
265impl ZstdThreads {
266    /// Creates a new [`ZstdThreads`] from a [`u32`].
267    pub fn new(threads: u32) -> Self {
268        Self(threads)
269    }
270
271    /// Creates a new [`ZstdThreads`] which uses all physical CPU cores.
272    ///
273    /// This is short for calling [`ZstdThreads::new`] with `0`.
274    pub fn all() -> Self {
275        Self(0)
276    }
277}
278
279impl Default for ZstdThreads {
280    /// Returns the default thread value (1) when compressing with zstd.
281    ///
282    /// The default adheres to the one selected by the [zstd] executable.
283    ///
284    /// [zstd]: https://man.archlinux.org/man/zstd.1
285    fn default() -> Self {
286        Self(1)
287    }
288}
289
290/// Settings for a compression encoder.
291#[derive(Clone, Debug, Eq, PartialEq)]
292pub enum CompressionSettings {
293    /// Settings for the bzip2 compression algorithm.
294    Bzip2 {
295        /// The used compression level.
296        compression_level: Bzip2CompressionLevel,
297    },
298
299    /// Settings for the gzip compression algorithm.
300    Gzip {
301        /// The used compression level.
302        compression_level: GzipCompressionLevel,
303    },
304
305    /// Settings for the xz compression algorithm.
306    Xz {
307        /// The used compression level.
308        compression_level: XzCompressionLevel,
309    },
310
311    /// Settings for the zstandard compression algorithm.
312    Zstd {
313        /// The used compression level.
314        compression_level: ZstdCompressionLevel,
315        /// The amount of threads to use when compressing.
316        threads: ZstdThreads,
317    },
318}
319
320impl Default for CompressionSettings {
321    /// Returns [`CompressionSettings::Zstd`].
322    ///
323    /// Defaults for `compression_level` and `threads` follow that of the [zstd] executable.
324    ///
325    /// [zstd]: https://man.archlinux.org/man/zstd.1
326    fn default() -> Self {
327        Self::Zstd {
328            compression_level: ZstdCompressionLevel::default(),
329            threads: ZstdThreads::default(),
330        }
331    }
332}
333
334impl From<&CompressionSettings> for CompressionAlgorithmFileExtension {
335    /// Creates a [`CompressionAlgorithmFileExtension`] from a [`CompressionSettings`].
336    fn from(value: &CompressionSettings) -> Self {
337        match value {
338            CompressionSettings::Bzip2 { .. } => CompressionAlgorithmFileExtension::Bzip2,
339            CompressionSettings::Gzip { .. } => CompressionAlgorithmFileExtension::Gzip,
340            CompressionSettings::Xz { .. } => CompressionAlgorithmFileExtension::Xz,
341            CompressionSettings::Zstd { .. } => CompressionAlgorithmFileExtension::Zstd,
342        }
343    }
344}
345
346/// Creates and configures an [`Encoder`].
347///
348/// Uses a dedicated `compression_level` and amount of `threads` to construct and configure an
349/// encoder for zstd compression.
350/// The `settings` are merely used for additional context in cases of error.
351///
352/// # Errors
353///
354/// Returns an error if
355///
356/// - the encoder cannot be created using the `file` and `compression_level`,
357/// - the encoder cannot be configured to use checksums at the end of each frame,
358/// - the amount of physical CPU cores can not be turned into a `u32`,
359/// - or multithreading can not be enabled based on the provided `threads` settings.
360fn create_zstd_encoder(
361    file: File,
362    compression_level: &ZstdCompressionLevel,
363    threads: &ZstdThreads,
364    settings: &CompressionSettings,
365) -> Result<Encoder<'static, File>, Error> {
366    let mut encoder = Encoder::new(file, compression_level.into()).map_err(|source| {
367        Error::CreateZstandardEncoder {
368            context: "initializing",
369            compression_settings: settings.clone(),
370            source,
371        }
372    })?;
373    // Include a context checksum at the end of each frame.
374    encoder
375        .include_checksum(true)
376        .map_err(|source| Error::CreateZstandardEncoder {
377            context: "setting checksums to be added",
378            compression_settings: settings.clone(),
379            source,
380        })?;
381
382    // Get amount of threads to use.
383    let threads = match threads {
384        // Use available physical CPU cores if the special value `0` is used.
385        // NOTE: For the zstd executable `0` means "use all available threads", while for the zstd
386        // crate this means "disable multithreading".
387        ZstdThreads(0) => {
388            u32::try_from(num_cpus::get_physical()).map_err(Error::IntegerConversion)?
389        }
390        ZstdThreads(threads) => *threads,
391    };
392
393    // Use multi-threading if it is available.
394    encoder
395        .multithread(threads)
396        .map_err(|source| Error::CreateZstandardEncoder {
397            context: "setting checksums to be added",
398            compression_settings: settings.clone(),
399            source,
400        })?;
401
402    Ok(encoder)
403}
404
405/// Encoder for compression which supports multiple backends.
406///
407/// Wraps [`BzEncoder`], [`GzEncoder`], [`XzEncoder`] and [`Encoder`].
408/// Provides a unified [`Write`] implementation across all of them.
409pub enum CompressionEncoder<'a> {
410    /// The bzip2 compression encoder.
411    Bzip2(BzEncoder<File>),
412
413    /// The gzip compression encoder.
414    Gzip(GzEncoder<File>),
415
416    /// The xz compression encoder.
417    Xz(XzEncoder<File>),
418
419    /// The zstd compression encoder.
420    Zstd(Encoder<'a, File>),
421}
422
423impl CompressionEncoder<'_> {
424    /// Creates a new [`CompressionEncoder`].
425    ///
426    /// Uses a [`File`] to stream to and initializes a specific backend based on the provided
427    /// [`CompressionSettings`].
428    ///
429    /// # Errors
430    ///
431    /// Returns an error if creating the encoder for zstd compression fails.
432    /// All other encoder initializations are infallible.
433    pub fn new(file: File, settings: &CompressionSettings) -> Result<Self, Error> {
434        Ok(match settings {
435            CompressionSettings::Bzip2 { compression_level } => Self::Bzip2(BzEncoder::new(
436                file,
437                bzip2::Compression::new(compression_level.into()),
438            )),
439            CompressionSettings::Gzip { compression_level } => Self::Gzip(GzEncoder::new(
440                file,
441                flate2::Compression::new(compression_level.into()),
442            )),
443            CompressionSettings::Xz { compression_level } => {
444                Self::Xz(XzEncoder::new_parallel(file, compression_level.into()))
445            }
446            CompressionSettings::Zstd {
447                compression_level,
448                threads,
449            } => Self::Zstd(create_zstd_encoder(
450                file,
451                compression_level,
452                threads,
453                settings,
454            )?),
455        })
456    }
457
458    /// Finishes the compression stream.
459    ///
460    /// # Error
461    ///
462    /// Returns an error if the wrapped encoder fails.
463    pub fn finish(self) -> Result<File, Error> {
464        match self {
465            CompressionEncoder::Bzip2(encoder) => {
466                encoder.finish().map_err(|source| Error::FinishEncoder {
467                    compression_type: CompressionAlgorithmFileExtension::Bzip2,
468                    source,
469                })
470            }
471            CompressionEncoder::Gzip(encoder) => {
472                encoder.finish().map_err(|source| Error::FinishEncoder {
473                    compression_type: CompressionAlgorithmFileExtension::Gzip,
474                    source,
475                })
476            }
477            CompressionEncoder::Xz(encoder) => {
478                encoder.finish().map_err(|source| Error::FinishEncoder {
479                    compression_type: CompressionAlgorithmFileExtension::Xz,
480                    source,
481                })
482            }
483            CompressionEncoder::Zstd(encoder) => {
484                encoder.finish().map_err(|source| Error::FinishEncoder {
485                    compression_type: CompressionAlgorithmFileExtension::Zstd,
486                    source,
487                })
488            }
489        }
490    }
491}
492
493impl Debug for CompressionEncoder<'_> {
494    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495        write!(
496            f,
497            "CompressionEncoder({})",
498            match self {
499                CompressionEncoder::Bzip2(_) => "Bzip2",
500                CompressionEncoder::Gzip(_) => "Gzip",
501                CompressionEncoder::Xz(_) => "Xz",
502                CompressionEncoder::Zstd(_) => "Zstd",
503            }
504        )
505    }
506}
507
508impl Write for CompressionEncoder<'_> {
509    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
510        match self {
511            CompressionEncoder::Bzip2(encoder) => encoder.write(buf),
512            CompressionEncoder::Gzip(encoder) => encoder.write(buf),
513            CompressionEncoder::Xz(encoder) => encoder.write(buf),
514            CompressionEncoder::Zstd(encoder) => encoder.write(buf),
515        }
516    }
517
518    fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
519        match self {
520            CompressionEncoder::Bzip2(encoder) => encoder.write_vectored(bufs),
521            CompressionEncoder::Gzip(encoder) => encoder.write_vectored(bufs),
522            CompressionEncoder::Xz(encoder) => encoder.write_vectored(bufs),
523            CompressionEncoder::Zstd(encoder) => encoder.write_vectored(bufs),
524        }
525    }
526
527    fn flush(&mut self) -> std::io::Result<()> {
528        match self {
529            CompressionEncoder::Bzip2(encoder) => encoder.flush(),
530            CompressionEncoder::Gzip(encoder) => encoder.flush(),
531            CompressionEncoder::Xz(encoder) => encoder.flush(),
532            CompressionEncoder::Zstd(encoder) => encoder.flush(),
533        }
534    }
535
536    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
537        match self {
538            CompressionEncoder::Bzip2(encoder) => encoder.write_all(buf),
539            CompressionEncoder::Gzip(encoder) => encoder.write_all(buf),
540            CompressionEncoder::Xz(encoder) => encoder.write_all(buf),
541            CompressionEncoder::Zstd(encoder) => encoder.write_all(buf),
542        }
543    }
544
545    fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> {
546        match self {
547            CompressionEncoder::Bzip2(encoder) => encoder.write_fmt(fmt),
548            CompressionEncoder::Gzip(encoder) => encoder.write_fmt(fmt),
549            CompressionEncoder::Xz(encoder) => encoder.write_fmt(fmt),
550            CompressionEncoder::Zstd(encoder) => encoder.write_fmt(fmt),
551        }
552    }
553
554    fn by_ref(&mut self) -> &mut Self
555    where
556        Self: Sized,
557    {
558        self
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use std::io::IoSlice;
565
566    use proptest::{proptest, test_runner::Config as ProptestConfig};
567    use rstest::rstest;
568    use tempfile::tempfile;
569    use testresult::TestResult;
570
571    use super::*;
572
573    proptest! {
574        #![proptest_config(ProptestConfig::with_cases(1000))]
575
576        #[test]
577        fn valid_bzip2_compression_level_try_from_i8(input in 1..=9i8) {
578            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
579        }
580
581        #[test]
582        fn valid_bzip2_compression_level_try_from_i16(input in 1..=9i16) {
583            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
584        }
585
586        #[test]
587        fn valid_bzip2_compression_level_try_from_i32(input in 1..=9i32) {
588            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
589        }
590
591        #[test]
592        fn valid_bzip2_compression_level_try_from_i64(input in 1..=9i64) {
593            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
594        }
595
596        #[test]
597        fn valid_bzip2_compression_level_try_from_u8(input in 1..=9u8) {
598            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
599        }
600
601        #[test]
602        fn valid_bzip2_compression_level_try_from_u16(input in 1..=9u16) {
603            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
604        }
605
606        #[test]
607        fn valid_bzip2_compression_level_try_from_u32(input in 1..=9u32) {
608            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
609        }
610
611        #[test]
612        fn valid_bzip2_compression_level_try_from_u64(input in 1..=9u64) {
613            assert!(Bzip2CompressionLevel::try_from(input).is_ok());
614        }
615
616        #[test]
617        fn valid_gzip_compression_level_try_from_i8(input in 1..=9i8) {
618            assert!(GzipCompressionLevel::try_from(input).is_ok());
619        }
620
621        #[test]
622        fn valid_gzip_compression_level_try_from_i16(input in 1..=9i16) {
623            assert!(GzipCompressionLevel::try_from(input).is_ok());
624        }
625
626        #[test]
627        fn valid_gzip_compression_level_try_from_i32(input in 1..=9i32) {
628            assert!(GzipCompressionLevel::try_from(input).is_ok());
629        }
630
631        #[test]
632        fn valid_gzip_compression_level_try_from_i64(input in 1..=9i64) {
633            assert!(GzipCompressionLevel::try_from(input).is_ok());
634        }
635
636        #[test]
637        fn valid_gzip_compression_level_try_from_u8(input in 1..=9u8) {
638            assert!(GzipCompressionLevel::try_from(input).is_ok());
639        }
640
641        #[test]
642        fn valid_gzip_compression_level_try_from_u16(input in 1..=9u16) {
643            assert!(GzipCompressionLevel::try_from(input).is_ok());
644        }
645
646        #[test]
647        fn valid_gzip_compression_level_try_from_u32(input in 1..=9u32) {
648            assert!(GzipCompressionLevel::try_from(input).is_ok());
649        }
650
651        #[test]
652        fn valid_gzip_compression_level_try_from_u64(input in 1..=9u64) {
653            assert!(GzipCompressionLevel::try_from(input).is_ok());
654        }
655
656        #[test]
657        fn valid_xz_compression_level_try_from_i8(input in 0..=9i8) {
658            assert!(XzCompressionLevel::try_from(input).is_ok());
659        }
660
661        #[test]
662        fn valid_xz_compression_level_try_from_i16(input in 0..=9i16) {
663            assert!(XzCompressionLevel::try_from(input).is_ok());
664        }
665
666        #[test]
667        fn valid_xz_compression_level_try_from_i32(input in 0..=9i32) {
668            assert!(XzCompressionLevel::try_from(input).is_ok());
669        }
670
671        #[test]
672        fn valid_xz_compression_level_try_from_i64(input in 0..=9i64) {
673            assert!(XzCompressionLevel::try_from(input).is_ok());
674        }
675
676        #[test]
677        fn valid_xz_compression_level_try_from_u8(input in 0..=9u8) {
678            assert!(XzCompressionLevel::try_from(input).is_ok());
679        }
680
681        #[test]
682        fn valid_xz_compression_level_try_from_u16(input in 0..=9u16) {
683            assert!(XzCompressionLevel::try_from(input).is_ok());
684        }
685
686        #[test]
687        fn valid_xz_compression_level_try_from_u32(input in 0..=9u32) {
688            assert!(XzCompressionLevel::try_from(input).is_ok());
689        }
690
691        #[test]
692        fn valid_xz_compression_level_try_from_u64(input in 0..=9u64) {
693            assert!(XzCompressionLevel::try_from(input).is_ok());
694        }
695
696        #[test]
697        fn valid_zstd_compression_level_try_from_i8(input in 0..=22i8) {
698            assert!(ZstdCompressionLevel::try_from(input).is_ok());
699        }
700
701        #[test]
702        fn valid_zstd_compression_level_try_from_i16(input in 0..=22i16) {
703            assert!(ZstdCompressionLevel::try_from(input).is_ok());
704        }
705
706        #[test]
707        fn valid_zstd_compression_level_try_from_i32(input in 0..=22i32) {
708            assert!(ZstdCompressionLevel::try_from(input).is_ok());
709        }
710
711        #[test]
712        fn valid_zstd_compression_level_try_from_i64(input in 0..=22i64) {
713            assert!(ZstdCompressionLevel::try_from(input).is_ok());
714        }
715
716        #[test]
717        fn valid_zstd_compression_level_try_from_u8(input in 0..=22u8) {
718            assert!(ZstdCompressionLevel::try_from(input).is_ok());
719        }
720
721        #[test]
722        fn valid_zstd_compression_level_try_from_u16(input in 0..=22u16) {
723            assert!(ZstdCompressionLevel::try_from(input).is_ok());
724        }
725
726        #[test]
727        fn valid_zstd_compression_level_try_from_u32(input in 0..=22u32) {
728            assert!(ZstdCompressionLevel::try_from(input).is_ok());
729        }
730
731        #[test]
732        fn valid_zstd_compression_level_try_from_u64(input in 0..=22u64) {
733            assert!(ZstdCompressionLevel::try_from(input).is_ok());
734        }
735    }
736
737    #[rstest]
738    #[case::too_large(Bzip2CompressionLevel::max() + 1)]
739    #[case::too_small(Bzip2CompressionLevel::min() - 1)]
740    fn create_bzip2_compression_level_fails(#[case] level: u8) -> TestResult {
741        if let Ok(level) = Bzip2CompressionLevel::new(level) {
742            return Err(format!("Should not have succeeded but created level: {level}").into());
743        }
744
745        Ok(())
746    }
747
748    #[test]
749    fn create_bzip2_compression_level_succeeds() -> TestResult {
750        if let Err(error) = Bzip2CompressionLevel::new(6) {
751            return Err(format!("Should have succeeded but raised error:\n{error}").into());
752        }
753
754        Ok(())
755    }
756
757    #[rstest]
758    #[case::too_large(GzipCompressionLevel::max() + 1)]
759    #[case::too_small(GzipCompressionLevel::min() - 1)]
760    fn create_gzip_compression_level_fails(#[case] level: u8) -> TestResult {
761        if let Ok(level) = GzipCompressionLevel::new(level) {
762            return Err(format!("Should not have succeeded but created level: {level}").into());
763        }
764
765        Ok(())
766    }
767
768    #[test]
769    fn create_gzip_compression_level_succeeds() -> TestResult {
770        if let Err(error) = GzipCompressionLevel::new(6) {
771            return Err(format!("Should have succeeded but raised error:\n{error}").into());
772        }
773
774        Ok(())
775    }
776
777    #[test]
778    fn create_xz_compression_level_fails() -> TestResult {
779        if let Ok(level) = XzCompressionLevel::new(XzCompressionLevel::max() + 1) {
780            return Err(format!("Should not have succeeded but created level: {level}").into());
781        }
782
783        Ok(())
784    }
785
786    #[test]
787    fn create_xz_compression_level_succeeds() -> TestResult {
788        if let Err(error) = XzCompressionLevel::new(6) {
789            return Err(format!("Should have succeeded but raised error:\n{error}").into());
790        }
791
792        Ok(())
793    }
794
795    #[test]
796    fn create_zstd_compression_level_fails() -> TestResult {
797        if let Ok(level) = ZstdCompressionLevel::new(ZstdCompressionLevel::max() + 1) {
798            return Err(format!("Should not have succeeded but created level: {level}").into());
799        }
800
801        Ok(())
802    }
803
804    #[test]
805    fn create_zstd_compression_level_succeeds() -> TestResult {
806        if let Err(error) = ZstdCompressionLevel::new(6) {
807            return Err(format!("Should have succeeded but raised error:\n{error}").into());
808        }
809
810        Ok(())
811    }
812
813    /// Ensures that the default [`CompressionSettings`] are those for zstd.
814    #[test]
815    fn default_compression_settings() -> TestResult {
816        assert!(matches!(
817            CompressionSettings::default(),
818            CompressionSettings::Zstd {
819                compression_level: _,
820                threads: _,
821            }
822        ));
823        Ok(())
824    }
825
826    /// Ensures that the [`Write::write`] implementation works for each [`CompressionEncoder`].
827    #[rstest]
828    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
829    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
830    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
831    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
832    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
833    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
834    fn test_compression_encoder_write(#[case] settings: CompressionSettings) -> TestResult {
835        let file = tempfile()?;
836        let mut encoder = CompressionEncoder::new(file, &settings)?;
837        let ref_encoder = encoder.by_ref();
838        let buf = &[1; 8];
839
840        let mut write_len = 0;
841        while write_len < buf.len() {
842            let len_written = ref_encoder.write(buf)?;
843            write_len += len_written;
844        }
845
846        ref_encoder.flush()?;
847
848        Ok(())
849    }
850
851    /// Ensures that the [`Write::write_vectored`] implementation works for each
852    /// [`CompressionEncoder`].
853    #[rstest]
854    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
855    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
856    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
857    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
858    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
859    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
860    fn test_compression_encoder_write_vectored(
861        #[case] settings: CompressionSettings,
862    ) -> TestResult {
863        let file = tempfile()?;
864        let mut encoder = CompressionEncoder::new(file, &settings)?;
865        let ref_encoder = encoder.by_ref();
866
867        let data1 = [1; 8];
868        let data2 = [15; 8];
869        let io_slice1 = IoSlice::new(&data1);
870        let io_slice2 = IoSlice::new(&data2);
871
872        let mut write_len = 0;
873        while write_len < data1.len() + data2.len() {
874            let len_written = ref_encoder.write_vectored(&[io_slice1, io_slice2])?;
875            write_len += len_written;
876        }
877
878        ref_encoder.flush()?;
879
880        Ok(())
881    }
882
883    /// Ensures that the [`Write::write_all`] implementation works for each [`CompressionEncoder`].
884    #[rstest]
885    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
886    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
887    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
888    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
889    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
890    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
891    fn test_compression_encoder_write_all(#[case] settings: CompressionSettings) -> TestResult {
892        let file = tempfile()?;
893        let mut encoder = CompressionEncoder::new(file, &settings)?;
894        let ref_encoder = encoder.by_ref();
895        let buf = &[1; 8];
896
897        ref_encoder.write_all(buf)?;
898
899        ref_encoder.flush()?;
900
901        Ok(())
902    }
903
904    /// Ensures that the [`Write::write_fmt`] implementation works for each [`CompressionEncoder`].
905    #[rstest]
906    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default()})]
907    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default()})]
908    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default()})]
909    #[case::zstd_all_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(0) })]
910    #[case::zstd_one_thread(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(1) })]
911    #[case::zstd_crazy_threads(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::new(99999) })]
912    fn test_compression_encoder_write_fmt(#[case] settings: CompressionSettings) -> TestResult {
913        let file = tempfile()?;
914        let mut encoder = CompressionEncoder::new(file, &settings)?;
915        let ref_encoder = encoder.by_ref();
916
917        ref_encoder.write_fmt(format_args!("{:.*}", 2, 1.234567))?;
918
919        ref_encoder.flush()?;
920
921        Ok(())
922    }
923}