use std::io::{self, BufWriter, Write};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub(crate) enum Codec {
#[default]
None,
Zstd,
Gzip,
}
impl Codec {
pub(crate) fn parse(name: &str) -> anyhow::Result<Self> {
match name {
"none" => Ok(Self::None),
"zstd" => Ok(Self::Zstd),
"gzip" => Ok(Self::Gzip),
other => anyhow::bail!("unknown --compress codec: {other}"),
}
}
pub(crate) fn default_level(self) -> i32 {
match self {
Self::None => 0,
Self::Zstd => 6,
Self::Gzip => 6,
}
}
fn level_range(self) -> (i32, i32) {
match self {
Self::None => (0, 0),
Self::Zstd => (-7, 22),
Self::Gzip => (0, 9),
}
}
pub(crate) fn extension(self) -> &'static str {
match self {
Self::None => "",
Self::Zstd => ".zst",
Self::Gzip => ".gz",
}
}
pub(crate) fn name(self) -> &'static str {
match self {
Self::None => "none",
Self::Zstd => "zstd",
Self::Gzip => "gzip",
}
}
}
pub(crate) fn check_level(codec: Codec, level: Option<i32>) -> anyhow::Result<i32> {
let Some(level) = level else {
return Ok(codec.default_level());
};
if codec == Codec::None {
anyhow::bail!(
"--compress-level needs a codec; pass `--compress zstd` or `--compress gzip`"
);
}
let (lo, hi) = codec.level_range();
if level < lo || level > hi {
anyhow::bail!(
"--compress-level {level} is outside {}'s range {lo}..={hi}",
codec.name()
);
}
Ok(level)
}
pub(crate) enum Encoder<W: Write> {
Plain(W),
Zstd(Box<zstd::Encoder<'static, W>>),
Gzip(flate2::write::GzEncoder<W>),
}
impl<W: Write> Write for Encoder<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Plain(w) => w.write(buf),
Self::Zstd(e) => e.write(buf),
Self::Gzip(e) => e.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Plain(w) => w.flush(),
Self::Zstd(e) => e.flush(),
Self::Gzip(e) => e.flush(),
}
}
}
pub(crate) type Sink<W> = BufWriter<Encoder<W>>;
pub(crate) fn open<W: Write>(w: W, codec: Codec, level: i32) -> io::Result<Sink<W>> {
let enc = match codec {
Codec::None => Encoder::Plain(w),
Codec::Zstd => Encoder::Zstd(Box::new(zstd::Encoder::new(w, level)?)),
Codec::Gzip => Encoder::Gzip(flate2::write::GzEncoder::new(
w,
flate2::Compression::new(level.clamp(0, 9) as u32),
)),
};
Ok(BufWriter::new(enc))
}
pub(crate) fn close<W: Write>(sink: Sink<W>) -> io::Result<W> {
let enc = sink.into_inner().map_err(|e| e.into_error())?;
match enc {
Encoder::Plain(mut w) => {
w.flush()?;
Ok(w)
}
Encoder::Zstd(e) => e.finish(),
Encoder::Gzip(e) => e.finish(),
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Failing {
budget: usize,
written: usize,
}
impl Write for Failing {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.written >= self.budget {
return Err(io::Error::other("disk full"));
}
let n = buf.len().min(self.budget - self.written);
self.written += n;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn roundtrip(codec: Codec, level: i32, payload: &[u8]) -> Vec<u8> {
let mut sink = open(Vec::new(), codec, level).unwrap();
sink.write_all(payload).unwrap();
close(sink).unwrap()
}
#[test]
fn none_is_a_pass_through() {
assert_eq!(roundtrip(Codec::None, 0, b"hello"), b"hello");
}
#[test]
fn zstd_output_decodes_back_to_the_input() {
let payload: Vec<u8> =
std::iter::repeat_n(b"<http://ex/s> <http://ex/p> <http://ex/o> .\n", 5000)
.flatten()
.copied()
.collect();
let out = roundtrip(Codec::Zstd, Codec::Zstd.default_level(), &payload);
assert!(
out.len() < payload.len() / 10,
"should compress: {}",
out.len()
);
assert_eq!(&out[..4], &[0x28, 0xB5, 0x2F, 0xFD]);
assert_eq!(zstd::decode_all(&out[..]).unwrap(), payload);
}
#[test]
fn gzip_output_decodes_back_to_the_input() {
use std::io::Read;
let payload: Vec<u8> =
std::iter::repeat_n(b"<http://ex/s> <http://ex/p> <http://ex/o> .\n", 5000)
.flatten()
.copied()
.collect();
let out = roundtrip(Codec::Gzip, Codec::Gzip.default_level(), &payload);
assert_eq!(&out[..2], &[0x1F, 0x8B], "gzip magic");
let mut got = Vec::new();
flate2::read::GzDecoder::new(&out[..])
.read_to_end(&mut got)
.unwrap();
assert_eq!(got, payload);
}
#[test]
fn a_dropped_encoder_produces_a_frame_that_does_not_decode() {
let payload: Vec<u8> = std::iter::repeat_n(b"abcdefghij", 5000)
.flatten()
.copied()
.collect();
let finished = roundtrip(Codec::Zstd, 3, &payload);
assert_eq!(zstd::decode_all(&finished[..]).unwrap(), payload);
let abandoned = {
struct Shared(std::rc::Rc<std::cell::RefCell<Vec<u8>>>);
impl Write for Shared {
fn write(&mut self, b: &[u8]) -> io::Result<usize> {
self.0.borrow_mut().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let buf = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
{
let mut sink = open(Shared(buf.clone()), Codec::Zstd, 3).unwrap();
sink.write_all(&payload).unwrap();
sink.flush().unwrap();
drop(sink);
}
let v = buf.borrow().clone();
v
};
assert!(
zstd::decode_all(&abandoned[..]).is_err(),
"an unfinished frame must NOT decode — if this ever passes, the \
truncation bug close() guards against has become invisible"
);
assert_ne!(abandoned, finished);
}
#[test]
fn a_write_failure_surfaces_from_close_rather_than_being_swallowed() {
for codec in [Codec::None, Codec::Zstd, Codec::Gzip] {
let mut sink = open(
Failing {
budget: 8,
written: 0,
},
codec,
codec.default_level(),
)
.unwrap();
let big = vec![b'x'; 1 << 20];
let wrote = sink.write_all(&big);
let closed = close(sink);
assert!(
wrote.is_err() || closed.is_err(),
"{codec:?}: a failing sink must produce an error somewhere"
);
}
}
#[test]
fn level_validation_rejects_what_the_codec_would_not_accept() {
assert_eq!(check_level(Codec::Zstd, None).unwrap(), 6);
assert_eq!(check_level(Codec::Gzip, None).unwrap(), 6);
assert_eq!(check_level(Codec::Zstd, Some(1)).unwrap(), 1);
assert_eq!(check_level(Codec::Zstd, Some(-5)).unwrap(), -5);
assert!(check_level(Codec::Zstd, Some(23)).is_err());
assert!(check_level(Codec::Gzip, Some(10)).is_err());
assert!(check_level(Codec::None, Some(3)).is_err());
assert_eq!(check_level(Codec::None, None).unwrap(), 0);
}
#[test]
fn every_level_in_range_produces_a_decodable_frame() {
let payload: Vec<u8> = std::iter::repeat_n(b"<http://ex/s> <http://ex/p> \"v\" .\n", 2000)
.flatten()
.copied()
.collect();
for level in [-7, -1, 1, 3, 6, 12, 19, 22] {
let out = roundtrip(Codec::Zstd, level, &payload);
assert_eq!(
zstd::decode_all(&out[..]).unwrap(),
payload,
"zstd level {level}"
);
}
}
}