alpm_compress/tarball/
builder.rs

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