blob: df3a98ae789b0bf6d45915937220f434375aad7d [file] [log] [blame]
use std::fmt;
use std::io::{Read, Write};
use std::path::Path;
use std::str::FromStr;
use anyhow::{Context, Error};
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use rayon::prelude::*;
use xz2::read::XzDecoder;
use xz2::write::XzEncoder;
#[derive(Default, Debug, Copy, Clone)]
pub enum CompressionProfile {
NoOp,
Fast,
#[default]
Balanced,
Best,
}
impl FromStr for CompressionProfile {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Error> {
Ok(match input {
"fast" => Self::Fast,
"balanced" => Self::Balanced,
"best" => Self::Best,
"no-op" => Self::NoOp,
other => anyhow::bail!("invalid compression profile: {other}"),
})
}
}
impl fmt::Display for CompressionProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompressionProfile::Fast => f.write_str("fast"),
CompressionProfile::Balanced => f.write_str("balanced"),
CompressionProfile::Best => f.write_str("best"),
CompressionProfile::NoOp => f.write_str("no-op"),
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum CompressionFormat {
Gz,
Xz,
}
impl CompressionFormat {
pub(crate) fn detect_from_path(path: impl AsRef<Path>) -> Option<Self> {
match path.as_ref().extension().and_then(|e| e.to_str()) {
Some("gz") => Some(CompressionFormat::Gz),
Some("xz") => Some(CompressionFormat::Xz),
_ => None,
}
}
pub(crate) fn extension(&self) -> &'static str {
match self {
CompressionFormat::Gz => "gz",
CompressionFormat::Xz => "xz",
}
}
pub(crate) fn encode(
&self,
path: impl AsRef<Path>,
profile: CompressionProfile,
) -> Result<Box<dyn Encoder>, Error> {
let mut os = path.as_ref().as_os_str().to_os_string();
os.push(format!(".{}", self.extension()));
let path = Path::new(&os);
if path.exists() {
crate::util::remove_file(path)?;
}
let file = crate::util::create_new_file(path)?;
Ok(match self {
CompressionFormat::Gz => Box::new(GzEncoder::new(
file,
match profile {
CompressionProfile::Fast => flate2::Compression::fast(),
CompressionProfile::Balanced => flate2::Compression::new(6),
CompressionProfile::Best => flate2::Compression::best(),
CompressionProfile::NoOp => panic!(
"compression profile 'no-op' should not call `CompressionFormat::encode`."
),
},
)),
CompressionFormat::Xz => {
let encoder = match profile {
CompressionProfile::NoOp => panic!(
"compression profile 'no-op' should not call `CompressionFormat::encode`."
),
CompressionProfile::Fast => {
xz2::stream::MtStreamBuilder::new().threads(6).preset(1).encoder().unwrap()
}
CompressionProfile::Balanced => {
xz2::stream::MtStreamBuilder::new().threads(6).preset(6).encoder().unwrap()
}
CompressionProfile::Best => {
// Note that this isn't actually the best compression settings for the
// produced artifacts, the production artifacts on static.rust-lang.org are
// produced by rust-lang/promote-release which hosts recompression logic
// and is tuned for optimal compression.
xz2::stream::MtStreamBuilder::new().threads(6).preset(9).encoder().unwrap()
}
};
let compressor = XzEncoder::new_stream(std::io::BufWriter::new(file), encoder);
Box::new(compressor)
}
})
}
pub(crate) fn decode(&self, path: impl AsRef<Path>) -> Result<Box<dyn Read>, Error> {
let file = crate::util::open_file(path.as_ref())?;
Ok(match self {
CompressionFormat::Gz => Box::new(GzDecoder::new(file)),
CompressionFormat::Xz => Box::new(XzDecoder::new(file)),
})
}
}
/// This struct wraps Vec<CompressionFormat> in order to parse the value from the command line.
#[derive(Debug, Clone)]
pub struct CompressionFormats(Vec<CompressionFormat>);
impl TryFrom<&'_ str> for CompressionFormats {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut parsed = Vec::new();
for format in value.split(',') {
match format.trim() {
"gz" => parsed.push(CompressionFormat::Gz),
"xz" => parsed.push(CompressionFormat::Xz),
other => anyhow::bail!("unknown compression format: {}", other),
}
}
Ok(CompressionFormats(parsed))
}
}
impl FromStr for CompressionFormats {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(value)
}
}
impl fmt::Display for CompressionFormats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, format) in self.iter().enumerate() {
if i != 0 {
write!(f, ",")?;
}
fmt::Display::fmt(
match format {
CompressionFormat::Xz => "xz",
CompressionFormat::Gz => "gz",
},
f,
)?;
}
Ok(())
}
}
impl Default for CompressionFormats {
fn default() -> Self {
Self(vec![CompressionFormat::Gz, CompressionFormat::Xz])
}
}
impl CompressionFormats {
pub(crate) fn iter(&self) -> impl Iterator<Item = CompressionFormat> + '_ {
self.0.iter().copied()
}
}
pub(crate) trait Encoder: Send + Write {
fn finish(self: Box<Self>) -> Result<(), Error>;
}
impl<W: Send + Write> Encoder for GzEncoder<W> {
fn finish(self: Box<Self>) -> Result<(), Error> {
GzEncoder::finish(*self).context("failed to finish .gz file")?;
Ok(())
}
}
impl<W: Send + Write> Encoder for XzEncoder<W> {
fn finish(self: Box<Self>) -> Result<(), Error> {
XzEncoder::finish(*self).context("failed to finish .xz file")?;
Ok(())
}
}
pub(crate) struct CombinedEncoder {
encoders: Vec<Box<dyn Encoder>>,
}
impl CombinedEncoder {
pub(crate) fn new(encoders: Vec<Box<dyn Encoder>>) -> Box<dyn Encoder> {
Box::new(Self { encoders })
}
}
impl Write for CombinedEncoder {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.write_all(buf)?;
Ok(buf.len())
}
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
self.encoders.par_iter_mut().try_for_each(|w| w.write_all(buf))
}
fn flush(&mut self) -> std::io::Result<()> {
self.encoders.par_iter_mut().try_for_each(Write::flush)
}
}
impl Encoder for CombinedEncoder {
fn finish(self: Box<Self>) -> Result<(), Error> {
self.encoders.into_par_iter().try_for_each(Encoder::finish)
}
}