| 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) | |
| } | |
| } |