alpm_compress/tarball/
builder.rs

1//! Creation of tarballs.
2
3use std::{fmt, fmt::Debug, fs::File};
4
5use tar::Builder;
6
7use crate::{
8    Error,
9    compression::{CompressionEncoder, CompressionSettings},
10};
11
12/// Wraps a [`Builder`] that writes to a [`CompressionEncoder`].
13///
14/// As [`CompressionEncoder`] has an uncompressed variant, this can be used to create
15/// either compressed tarballs `.tar.*` or uncompressed tar archives `.tar`.
16pub struct TarballBuilder<'c> {
17    inner: Builder<CompressionEncoder<'c>>,
18}
19
20impl Debug for TarballBuilder<'_> {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        f.debug_struct("TarballBuilder")
23            .field("inner", &"Builder<CompressionEncoder>")
24            .finish()
25    }
26}
27
28impl<'c> TarballBuilder<'c> {
29    /// Creates a new [`TarballBuilder`] that writes to the given [`File`] with the given
30    /// [`CompressionSettings`].
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if [`CompressionEncoder`] initialization fails.
35    pub fn new(file: File, settings: &CompressionSettings) -> Result<Self, Error> {
36        CompressionEncoder::new(file, settings).map(Self::from)
37    }
38
39    /// Returns a mutable reference to the inner [`Builder`].
40    ///
41    /// This can be used to set options to the builder or append files to the tarball.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// # use tempfile::{tempfile, NamedTempFile};
47    /// # use alpm_compress::{tarball::TarballBuilder, compression::CompressionSettings};
48    /// # use testresult::TestResult;
49    /// # fn main() -> TestResult {
50    /// # let mut builder = TarballBuilder::new(tempfile()?, &CompressionSettings::None)?;
51    /// builder.inner_mut().follow_symlinks(false);
52    /// # Ok(())
53    /// # }
54    /// ```
55    pub fn inner_mut(&mut self) -> &mut Builder<CompressionEncoder<'c>> {
56        &mut self.inner
57    }
58
59    /// Finishes writing the tarball.
60    ///
61    /// Delegates to [`CompressionEncoder::finish`] of the inner [`Builder`].
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if the [`CompressionEncoder`] fails to finish the compression stream.
66    pub fn finish(self) -> Result<(), Error> {
67        self.inner
68            .into_inner()
69            .map_err(|source| Error::IoWrite {
70                context: "writing the archive",
71                source,
72            })?
73            .finish()?;
74        Ok(())
75    }
76}
77
78impl<'c> From<CompressionEncoder<'c>> for TarballBuilder<'c> {
79    /// Creates a [`TarballBuilder`] from a [`CompressionEncoder`].
80    fn from(encoder: CompressionEncoder<'c>) -> Self {
81        Self {
82            inner: Builder::new(encoder),
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use std::io::Write;
90
91    use rstest::rstest;
92    use tempfile::{NamedTempFile, tempfile};
93    use testresult::TestResult;
94
95    use super::*;
96    use crate::compression::{
97        Bzip2CompressionLevel,
98        CompressionSettings,
99        GzipCompressionLevel,
100        XzCompressionLevel,
101        ZstdCompressionLevel,
102        ZstdThreads,
103    };
104
105    #[rstest]
106    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default() })]
107    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default() })]
108    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default() })]
109    #[case::zstd(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::all() })]
110    #[case::no_compression(CompressionSettings::None)]
111    fn test_tarball_builder_write_file(
112        #[case] compression_settings: CompressionSettings,
113    ) -> TestResult {
114        let mut builder = TarballBuilder::new(tempfile()?, &compression_settings)?;
115        let test_file = NamedTempFile::new()?;
116        {
117            let mut f = test_file.reopen()?;
118            f.write_all(b"alpm4ever")?;
119            f.flush()?;
120        }
121
122        builder
123            .inner_mut()
124            .append_path_with_name(test_file.path(), "testfile")?;
125        builder.finish()?;
126
127        Ok(())
128    }
129
130    #[rstest]
131    #[case::bzip2(CompressionSettings::Bzip2 { compression_level: Bzip2CompressionLevel::default() })]
132    #[case::gzip(CompressionSettings::Gzip { compression_level: GzipCompressionLevel::default() })]
133    #[case::xz(CompressionSettings::Xz { compression_level: XzCompressionLevel::default() })]
134    #[case::zstd(CompressionSettings::Zstd { compression_level: ZstdCompressionLevel::default(), threads: ZstdThreads::all() })]
135    #[case::no_compression(CompressionSettings::None)]
136    fn test_tarball_builder_debug(#[case] compression_settings: CompressionSettings) -> TestResult {
137        let builder = TarballBuilder::new(tempfile()?, &compression_settings)?;
138        let dbg = format!("{:?}", builder);
139        assert!(dbg.contains("TarballBuilder"));
140        Ok(())
141    }
142}