Compression in Rust¶
For CLI tools and services that need to read or write these formats, the Rust ecosystem has mature crates for almost everything.
Crates¶
| Crate | Formats | Notes |
|---|---|---|
flate2 |
gzip, zlib, raw DEFLATE | Pure-Rust miniz_oxide backend by default; zlib-rs or zlib-ng backends for speed |
zstd |
zstd | Bindings to libzstd; multithreaded encoder (zstdmt feature), dictionaries |
xz2 / liblzma |
xz, lzma | Bindings to liblzma; liblzma is the maintained fork |
lzma-rs |
xz, lzma (decode) | Pure Rust |
bzip2 |
bzip2 | Bindings, with a pure-Rust backend option in newer versions |
lz4_flex |
lz4 | Pure Rust, fast |
brotli |
brotli | Pure Rust |
tar |
tar | Read/write over any Read/Write |
zip |
zip | Read/write, including AES and zstd/bzip2/deflate64 methods (feature flags) |
sevenz-rust2 |
7z | Read/write, including AES |
async-compression |
Most of the above | tokio/futures async wrappers |
The common pattern: compressors implement Read (decoders) and Write (encoders), so they wrap files, sockets, or each other, just like a shell pipeline.
Streaming .tar.zst¶
use std::fs::File;
use std::io;
fn create(src: &str, out: &str) -> io::Result<()> {
let file = File::create(out)?;
let mut enc = zstd::Encoder::new(file, 19)?;
enc.multithread(std::thread::available_parallelism()?.get() as u32)?;
let mut tar = tar::Builder::new(enc);
tar.append_dir_all(".", src)?;
tar.into_inner()?.finish()?;
Ok(())
}
fn extract(archive: &str, dest: &str) -> io::Result<()> {
let dec = zstd::Decoder::new(File::open(archive)?)?;
tar::Archive::new(dec).unpack(dest)?;
Ok(())
}
multithread requires the zstdmt feature on the zstd crate. Call finish() on encoders — dropping them without it can leave a truncated stream.
gzip¶
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use std::io::{copy, BufReader};
use std::fs::File;
fn gzip(input: &str, output: &str) -> std::io::Result<()> {
let mut reader = BufReader::new(File::open(input)?);
let mut enc = GzEncoder::new(File::create(output)?, Compression::best());
copy(&mut reader, &mut enc)?;
enc.finish()?;
Ok(())
}
fn gunzip(input: &str, output: &str) -> std::io::Result<()> {
let mut dec = GzDecoder::new(File::open(input)?);
copy(&mut dec, &mut File::create(output)?)?;
Ok(())
}
Use MultiGzDecoder for files made of concatenated gzip members.
Pick the codec from the extension¶
use std::{fs::File, io::{self, Read}, path::Path};
fn open_decompressed(path: &Path) -> io::Result<Box<dyn Read>> {
let f = File::open(path)?;
Ok(match path.extension().and_then(|e| e.to_str()) {
Some("gz") => Box::new(flate2::read::MultiGzDecoder::new(f)),
Some("zst") => Box::new(zstd::Decoder::new(f)?),
Some("xz") => Box::new(xz2::read::XzDecoder::new(f)),
Some("bz2") => Box::new(bzip2::read::BzDecoder::new(f)),
_ => Box::new(f),
})
}
Sniffing magic bytes is more robust than trusting extensions.
Safety when extracting¶
tar::Archive::unpackrefuses entries that escape the destination, but if you iterate entries and write them yourself, validate each path: reject absolute paths and..components.- With
zip, useZipFile::enclosed_name()rather than the raw name. - Cap total decompressed size and entry count when handling untrusted input, to defend against decompression bombs.
See gotchas.
See also¶
ouch— a ready-made Rust CLI for many formats (universal extractors)- zstd, tar, zip