cargo-deb 2.0.2

Make Debian packages (.deb) easily with a Cargo subcommand
Documentation
use crate::error::{CDResult, CargoDebError};
use std::io;
use std::io::{BufWriter, Read};
#[cfg(feature = "lzma")]
use std::num::NonZeroUsize;
use std::num::NonZeroU64;
use std::ops;
use std::process::{Child, ChildStdin};
use std::process::{Command, Stdio};

#[derive(Clone, Copy)]
pub enum Format {
    Xz,
    Gzip,
}

impl Format {
    #[must_use]
    pub fn extension(self) -> &'static str {
        match self {
            Self::Xz => "xz",
            Self::Gzip => "gz",
        }
    }

    fn program(self) -> &'static str {
        match self {
            Self::Xz => "xz",
            Self::Gzip => "gzip",
        }
    }

    fn level(self, fast: bool) -> u32 {
        match self {
            Self::Xz => if fast { 1 } else { 6 },
            Self::Gzip => if fast { 1 } else { 9 },
        }
    }
}

enum Writer {
    #[cfg(feature = "lzma")]
    Xz(xz2::write::XzEncoder<Vec<u8>>),
    Gz(flate2::write::GzEncoder<Vec<u8>>),
    ZopfliGz(BufWriter<zopfli::GzipEncoder<Vec<u8>>>),
    StdIn {
        compress_format: Format,
        child: Child,
        handle: std::thread::JoinHandle<io::Result<Vec<u8>>>,
        stdin: BufWriter<ChildStdin>,
    },
}

impl Writer {
    fn finish(self) -> io::Result<Compressed> {
        match self {
            #[cfg(feature = "lzma")]
            Self::Xz(w) => w.finish().map(|data| Compressed { compress_format: Format::Xz, data }),
            Self::StdIn {
                compress_format,
                mut child,
                handle,
                stdin,
            } => {
                drop(stdin);
                child.wait()?;
                handle.join().unwrap().map(|data| Compressed { compress_format, data })
            }
            Self::Gz(w) => w.finish().map(|data| Compressed { compress_format: Format::Gzip, data }),
            Self::ZopfliGz(w) => w.into_inner()?.finish().map(|data| Compressed { compress_format: Format::Gzip, data }),
        }
    }
}

pub struct Compressor {
    writer: Writer,
    pub uncompressed_size: usize,
}

impl io::Write for Compressor {
    fn flush(&mut self) -> io::Result<()> {
        match &mut self.writer {
            #[cfg(feature = "lzma")]
            Writer::Xz(w) => w.flush(),
            Writer::Gz(w) => w.flush(),
            Writer::ZopfliGz(w) => w.flush(),
            Writer::StdIn { stdin, .. } => stdin.flush(),
        }
    }

    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let len = match &mut self.writer {
            #[cfg(feature = "lzma")]
            Writer::Xz(w) => w.write(buf),
            Writer::Gz(w) => w.write(buf),
            Writer::ZopfliGz(w) => w.write(buf),
            Writer::StdIn { stdin, .. } => stdin.write(buf),
        }?;
        self.uncompressed_size += len;
        Ok(len)
    }

    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
        match &mut self.writer {
            #[cfg(feature = "lzma")]
            Writer::Xz(w) => w.write_all(buf),
            Writer::Gz(w) => w.write_all(buf),
            Writer::ZopfliGz(w) => w.write_all(buf),
            Writer::StdIn { stdin, .. } => stdin.write_all(buf),
        }?;
        self.uncompressed_size += buf.len();
        Ok(())
    }
}

impl Compressor {
    fn new(writer: Writer) -> Self {
        Self {
            writer,
            uncompressed_size: 0,
        }
    }

    pub fn finish(self) -> CDResult<Compressed> {
        self.writer.finish().map_err(From::from)
    }
}

pub struct Compressed {
    compress_format: Format,
    data: Vec<u8>,
}

impl Compressed {
    #[must_use]
    pub fn extension(&self) -> &'static str {
        self.compress_format.extension()
    }
}

impl ops::Deref for Compressed {
    type Target = Vec<u8>;

    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

fn system_compressor(compress_format: Format, fast: bool) -> CDResult<Compressor> {
    let mut child = Command::new(compress_format.program())
        .arg(format!("-{}", compress_format.level(fast)))
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit())
        .spawn()
        .map_err(|e| CargoDebError::CommandFailed(e, compress_format.program()))?;
    let mut stdout = child.stdout.take().unwrap();

    let handle = std::thread::spawn(move || {
        let mut buf = Vec::new();
        stdout.read_to_end(&mut buf).map(|_| buf)
    });

    let stdin = BufWriter::with_capacity(1<<16, child.stdin.take().unwrap());
    Ok(Compressor::new(Writer::StdIn { compress_format, child, handle, stdin }))
}


pub fn select_compressor(fast: bool, compress_format: Format, use_system: bool) -> CDResult<Compressor> {
    if use_system {
        return system_compressor(compress_format, fast);
    }

    match compress_format {
        #[cfg(feature = "lzma")]
        Format::Xz => {
            // Compression level 6 is a good trade off between size and [ridiculously] long compression time
            let encoder = xz2::stream::MtStreamBuilder::new()
                .threads(std::thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()).get() as u32)
                .preset(compress_format.level(fast))
                .encoder()
                .map_err(CargoDebError::LzmaCompressionError)?;

            let writer = xz2::write::XzEncoder::new_stream(Vec::new(), encoder);
            Ok(Compressor::new(Writer::Xz(writer)))
        }
        #[cfg(not(feature = "lzma"))]
        Format::Xz => system_compressor(compress_format, fast),
        Format::Gzip => {
            use zopfli::{GzipEncoder, Options, BlockType};
            use flate2::write::GzEncoder;
            use flate2::Compression;

            let writer = if !fast {
                let inner_writer = GzipEncoder::new_buffered(Options {
                    iteration_count: NonZeroU64::new(7).unwrap(),
                    ..Options::default()
                }, BlockType::Dynamic, Vec::new()).unwrap();
                Writer::ZopfliGz(inner_writer)
            } else {
               let inner_writer = GzEncoder::new(Vec::new(), Compression::new(compress_format.level(fast)));
               Writer::Gz(inner_writer)
            };
            Ok(Compressor::new(writer))
        }
    }
}