cargo_deb/util/
compress.rs

1use crate::error::{CDResult, CargoDebError};
2use std::io::{BufWriter, Read};
3#[cfg(feature = "lzma")]
4use std::num::NonZeroUsize;
5use std::process::{Child, ChildStdin, Command, Stdio};
6use std::{io, ops};
7use zopfli::{BlockType, GzipEncoder, Options};
8
9pub struct CompressConfig {
10    /// Don't compress heavily
11    pub fast: bool,
12    pub compress_type: Format,
13    pub compress_system: bool,
14    pub rsyncable: bool,
15}
16
17#[derive(Clone, Copy)]
18pub enum Format {
19    Xz,
20    Gzip,
21}
22
23impl Format {
24    #[must_use]
25    pub fn extension(self) -> &'static str {
26        match self {
27            Self::Xz => "xz",
28            Self::Gzip => "gz",
29        }
30    }
31
32    fn program(self) -> &'static str {
33        match self {
34            Self::Xz => "xz",
35            Self::Gzip => "gzip",
36        }
37    }
38
39    const fn level(self, fast: bool) -> u32 {
40        match self {
41            Self::Xz => if fast { 1 } else { 6 },
42            Self::Gzip => if fast { 1 } else { 9 },
43        }
44    }
45}
46
47enum Writer {
48    #[cfg(feature = "lzma")]
49    Xz(xz2::write::XzEncoder<Vec<u8>>),
50    #[cfg(feature = "gzip")]
51    Gz(flate2::write::GzEncoder<Vec<u8>>),
52    ZopfliGz(BufWriter<GzipEncoder<Vec<u8>>>),
53    StdIn {
54        compress_format: Format,
55        child: Child,
56        handle: std::thread::JoinHandle<io::Result<Vec<u8>>>,
57        stdin: BufWriter<ChildStdin>,
58    },
59}
60
61impl Writer {
62    fn finish(self) -> io::Result<Compressed> {
63        match self {
64            #[cfg(feature = "lzma")]
65            Self::Xz(w) => w.finish().map(|data| Compressed { compress_format: Format::Xz, data }),
66            Self::StdIn {
67                compress_format,
68                mut child,
69                handle,
70                stdin,
71            } => {
72                drop(stdin);
73                child.wait()?;
74                handle.join().unwrap().map(|data| Compressed { compress_format, data })
75            },
76            #[cfg(feature = "gzip")]
77            Self::Gz(w) => w.finish().map(|data| Compressed { compress_format: Format::Gzip, data }),
78            Self::ZopfliGz(w) => w.into_inner()?.finish().map(|data| Compressed { compress_format: Format::Gzip, data }),
79        }
80    }
81}
82
83pub struct Compressor {
84    writer: Writer,
85    pub uncompressed_size: usize,
86}
87
88impl io::Write for Compressor {
89    fn flush(&mut self) -> io::Result<()> {
90        match &mut self.writer {
91            #[cfg(feature = "lzma")]
92            Writer::Xz(w) => w.flush(),
93            #[cfg(feature = "gzip")]
94            Writer::Gz(w) => w.flush(),
95            Writer::ZopfliGz(w) => w.flush(),
96            Writer::StdIn { stdin, .. } => stdin.flush(),
97        }
98    }
99
100    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
101        let len = match &mut self.writer {
102            #[cfg(feature = "lzma")]
103            Writer::Xz(w) => w.write(buf),
104            #[cfg(feature = "gzip")]
105            Writer::Gz(w) => w.write(buf),
106            Writer::ZopfliGz(w) => w.write(buf),
107            Writer::StdIn { stdin, .. } => stdin.write(buf),
108        }?;
109        self.uncompressed_size += len;
110        Ok(len)
111    }
112
113    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
114        match &mut self.writer {
115            #[cfg(feature = "lzma")]
116            Writer::Xz(w) => w.write_all(buf),
117            #[cfg(feature = "gzip")]
118            Writer::Gz(w) => w.write_all(buf),
119            Writer::ZopfliGz(w) => w.write_all(buf),
120            Writer::StdIn { stdin, .. } => stdin.write_all(buf),
121        }?;
122        self.uncompressed_size += buf.len();
123        Ok(())
124    }
125}
126
127impl Compressor {
128    const fn new(writer: Writer) -> Self {
129        Self { writer, uncompressed_size: 0 }
130    }
131
132    pub fn finish(self) -> CDResult<Compressed> {
133        self.writer.finish().map_err(CargoDebError::Io)
134    }
135}
136
137pub struct Compressed {
138    compress_format: Format,
139    data: Vec<u8>,
140}
141
142impl Compressed {
143    #[must_use]
144    pub fn extension(&self) -> &'static str {
145        self.compress_format.extension()
146    }
147}
148
149impl ops::Deref for Compressed {
150    type Target = Vec<u8>;
151
152    fn deref(&self) -> &Self::Target {
153        &self.data
154    }
155}
156
157fn system_compressor(compress_format: Format, fast: bool) -> CDResult<Compressor> {
158    let mut child = Command::new(compress_format.program())
159        .arg(format!("-{}", compress_format.level(fast)))
160        .stdin(Stdio::piped())
161        .stdout(Stdio::piped())
162        .stderr(Stdio::inherit())
163        .spawn()
164        .map_err(|e| CargoDebError::CommandFailed(e, compress_format.program().into()))?;
165    let mut stdout = child.stdout.take().unwrap();
166
167    let handle = std::thread::spawn(move || {
168        let mut buf = Vec::new();
169        stdout.read_to_end(&mut buf).map(|_| buf)
170    });
171
172    let stdin = BufWriter::with_capacity(1<<16, child.stdin.take().unwrap());
173    Ok(Compressor::new(Writer::StdIn { compress_format, child, handle, stdin }))
174}
175
176pub fn select_compressor(fast: bool, compress_format: Format, use_system: bool) -> CDResult<Compressor> {
177    if use_system {
178        return system_compressor(compress_format, fast);
179    }
180
181    match compress_format {
182        #[cfg(feature = "lzma")]
183        Format::Xz => {
184            // Compression level 6 is a good trade off between size and [ridiculously] long compression time
185            let encoder = xz2::stream::MtStreamBuilder::new()
186                .threads(std::thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()).get() as u32)
187                .preset(compress_format.level(fast))
188                .encoder()
189                .map_err(CargoDebError::LzmaCompressionError)?;
190
191            let writer = xz2::write::XzEncoder::new_stream(Vec::new(), encoder);
192            Ok(Compressor::new(Writer::Xz(writer)))
193        },
194        #[cfg(not(feature = "lzma"))]
195        Format::Xz => system_compressor(compress_format, fast),
196        Format::Gzip => {
197            #[cfg(feature = "gzip")]
198            if fast {
199                let level = flate2::Compression::new(compress_format.level(fast));
200                let inner_writer = flate2::write::GzEncoder::new(Vec::new(), level);
201                return Ok(Compressor::new(Writer::Gz(inner_writer)));
202            }
203
204            let inner_writer = GzipEncoder::new_buffered(Options {
205                iteration_count: (if fast { 1 } else { 7 }).try_into().unwrap(),
206                ..Options::default()
207            }, BlockType::Dynamic, Vec::new()).unwrap();
208            Ok(Compressor::new(Writer::ZopfliGz(inner_writer)))
209        },
210    }
211}
212
213pub(crate) fn gzipped(mut content: &[u8]) -> io::Result<Vec<u8>> {
214    let mut compressed = Vec::new();
215    compressed.try_reserve(content.len() * 2 / 3)
216        .map_err(|_| io::ErrorKind::OutOfMemory)?;
217    let mut encoder = GzipEncoder::new(
218        Options {
219            iteration_count: 7.try_into().unwrap(),
220            ..Options::default()
221        },
222        BlockType::Dynamic,
223        &mut compressed,
224    )?;
225    io::copy(&mut content, &mut encoder)?;
226    encoder.finish()?;
227    Ok(compressed)
228}