An ecosystem-wide dependency audit found flate2 in 39 of 93 ~/work project lockfiles — not because anyone chose it, but because ureq’s default feature and the image crate’s png/tiff codecs quietly pulled it in underneath a Pure Rust workspace.
Today we released OxiArc 0.4.2 — the P2/P3 program: HTTP Content-Encoding decoding and three new image container formats, all Pure Rust, closing both routes for good. Five new crates — oxiarc-http, oxiarc-png, oxiarc-jpeg, oxiarc-tiff, oxiarc-image — bring the workspace from 13 to 18 member crates. No archive or stream wire format changed, and no public read/decode-path API was removed.
No C, no Fortran, no zlib, no libjpeg, no libpng, no libtiff, no external shared libraries. Just clean, memory-safe archiving, compression, and now HTTP content-coding and image decoding, that compiles to a single static binary, targets WASM, and runs everywhere. OxiArc is the Pure Rust replacement for the zip, tar, gzip, zstd, and 7-zip tools — and for the Rust crates zip, flate2, zstd, bzip2, lz4, tar, snap, brotli, miniz_oxide, png, jpeg-decoder/zune-jpeg, tiff, and now image itself.
Why OxiArc 0.4.2 is a game changer
ureq’s defaultgzipfeature meantflate2— and its C dependency chain — showed up in any project that just wanted to make an HTTP request. There was no Pure Rust way to decode a gzip-encoded response body without opting out of the convenience entirely.- The
imagecrate’s ownpngandtiffcodecs areflate2-backed, so any project usingimage::open()on those formats inherited the same problem one layer down. - Every streaming decoder in the workspace — gzip, zlib, Zstandard, Brotli — was actually
read_to_end-then-decode underneath.GzipStreamDecoder/ZlibStreamDecoder::fill_bufferdecoded the whole stream before serving the first byte; a second call after a mid-stream EOF could silently truncate output;oxiarc-zstdhad no output cap at all.
OxiArc 0.4.2 ends all of that:
- A rewritten, genuinely resumable DEFLATE/zlib/gzip core —
InflateStreamandWrappedInflate, plusInflateReader/AsyncInflateReader— replaces theread_to_end-then-decode decoders every streaming consumer used to be built on.oxiarc-zstdandoxiarc-brotligained the equivalent push decoders (ZstdStream,BrotliStream). oxiarc-httpdecodesContent-Encoding: gzip, deflate, br, zstd, chained codings, and the RFC 9842dcb/dczdictionary variants — with recipes forureq,reqwest, andoxihttpshipping as runnable examples.oxiarc-jpeg,oxiarc-png, andoxiarc-tiffare from-scratch Pure Rust decoders and encoders, each verified byte-identical tocjpeg/djpeg, Pillow, andtiffcp/tiffinfo/tifffilerespectively — not “close enough,” but a live reference-oracle suite from day one.oxiarc-imageis a thin,image-0.25-shaped facade over the three new codec crates, for the ecosystem projects that depend on theimageAPI surface rather than onpng/tiffdirectly.- Streaming allocation actually shrank. Metering
oxiarc-http’s legacy-.Zdecode bridge against its measured expansion ratio cut peak allocation on a 128 MiB, 4992:1 body from 134,605,718 bytes to 893,668 — with no throughput cost; interleaved A/B measurement showed the metered path ~8% faster, since the smaller working set fits cache.
Technical Deep Dive: one push-decoder shape, four formats
- The shared core.
InflateStream::inflate(&mut self, input, output, flush) -> InflateProgressis the same “feed bytes, get bytes, ask again” shape asZstdStream::decodeandBrotliStream’s meta-block-resumable loop — one push API serving both the archive/compression side (P2) and the new HTTP/image side (P3). - PNG’s
IDATchain and an HTTP chunked body are the same problem.oxiarc-pngandoxiarc-httpboth consumeInflateStream/WrappedInflatedirectly rather than each growing its own partial decoder — proven by a newtests/cross_crate_inflate.rsthat splits a PNGIDATchain, an HTTP gzip body, and a TIFF Deflate strip across arbitrary byte boundaries and checks all three decode to byte-identical output through the one shared core. - JPEG inside TIFF, without concatenating buffers.
oxiarc-jpeg’sTableSet/decode_abbreviated_intosurface was built specifically sooxiarc-tiff’sJPEGTables(compression 7) support could reuse the real decoder rather than reassembling a synthetic JPEG stream. - Negotiation is RFC text, not paraphrase.
oxiarc-http::negotiatehonours*andq=0per RFC 9110 §8.4.1.2, and the spec’s own example tables are runnable tests, not restated as prose.
Getting Started
cargo add oxiarc-http oxiarc-png
use oxiarc_http::{AcceptEncoding, ContentCoding, EncodeOptions, encode_body, negotiate};
fn main() {
// Client side: advertise every coding this build can decode.
let accept = AcceptEncoding::all_supported();
let header_value = accept.to_header_value(); // None => send no header at all
// Server side: negotiate against what it received, and only what this
// build can actually produce.
let available: Vec<ContentCoding> = [
ContentCoding::Zstd,
ContentCoding::Brotli,
ContentCoding::Gzip,
ContentCoding::Deflate,
]
.into_iter()
.filter(ContentCoding::is_encodable)
.collect();
let chosen = negotiate(header_value.as_deref(), &available)
.expect("identity is always acceptable here, so this never fails");
let body = b"hello, world! hello, world! hello, world!";
if let Some(coding) = chosen {
let compressed = encode_body(&coding, body, EncodeOptions::default())
.expect("`available` only ever contains codings this build can encode");
assert!(compressed.len() < body.len());
}
}
use std::fs::File;
fn main() -> Result<(), oxiarc_png::DecodingError> {
let decoder = oxiarc_png::Decoder::new(File::open("image.png")?);
let mut reader = decoder.read_info()?;
while let Some(row) = reader.next_row()? {
let _pixels: &[u8] = row.data();
}
Ok(())
}
What’s New in 0.4.2
- Added: Five new crates —
oxiarc-http(Content-Encoding/Accept-Encoding negotiation and decoding),oxiarc-png,oxiarc-jpeg(baseline, progressive, arithmetic coding, lossless, scaled decode),oxiarc-tiff(all baseline/extension codecs + CCITT G3/G4 including a new uncompressed mode), andoxiarc-image(animage-0.25-shaped facade). - Added:
oxiarc-brotlishared-dictionary support (both directions) andContent-Encoding: dcb/dcz(RFC 9842); the legacy UNIXcompress/.Zcontainer inoxiarc-lzw::z, exposed asContent-Encoding: compress. - Changed: A from-scratch DEFLATE encoder rewrite in
oxiarc-deflate— byte-identical to CPython’szlib.compressat every level 1-9. - Changed:
oxiarc-zstddecode-throughput rebuild around the reference decoder’s data layout;oxiarc-lzw’s TIFF/GIF decoder rebuilt to libtiff’s own algorithm shape. - Security: A Zstandard offset-code-31 computation that could overflow
usizeon 32-bit targets (wasm32, armv7, i686) is now computed inu64; anoxiarc-cli --memory-limitinteger-overflow on an adversarial SI byte-size string is fixed withchecked_mul. - Fixed:
oxiarc-zstd’s legacy one-shot decoders (decompress,decompress_multi_frame,ZstdDecoder::decode_frame) now reject the same malformed-frame classes the streaming decoder already refused, on shared code so the two paths can’t drift apart again. - 5,505 tests passing, 0 failed, 0 skipped (5,159 via
cargo nextest run --workspace --all-features+ 346 doctests); zero clippy warnings on every crate individually and workspace-wide (--all-featuresand--no-default-features);cargo deny check bansclean with new PNG/JPEG/TIFF/imagebans in place. 716 files, 227,104 Rust code lines workspace-wide.
Tips
- If you construct
LzwConfigwith a struct literal, add abit_orderfield. This is the one breaking change that reaches real downstream callers —LzwConfig::GIFwas accidentally MSB-first before this release (now correctly LSB-first), so the field exists to make the packing direction explicit rather than implicit and wrong. oxiarc-http’s default features aregzip+deflateonly.br,zstd,compress, and the RFC 9842dcb/dczcodings each need their own Cargo feature, so a default-feature client never advertises a coding it cannot decode — check the feature matrix inoxiarc-http/README.mdbefore assumingAcceptEncoding::all_supported()includes everything.- If you call
oxiarc-zstd::decompress_multi_frame(or its_with_dicttwin) on untrusted input, check the error instead of expectingOk(vec![]). Leading garbage or a truncated skippable-frame prefix is now a real error rather than a silent empty result — trailing-garbage tolerance is unchanged. - Migrating off
image,tiff,jpeg-decoder/zune-jpeg, orpng? Each new crate ships a compat facade shaped like the real crate’s API (oxiarc-tiff’scompatfeature,oxiarc-jpeg’scompat::zune/compat::jpeg_decoder) — a smaller diff than adopting the native API cold. - Streaming an HTTP body through
.Z/compressat a high expansion ratio? The bridge inoxiarc-httpnow retunes its read size against the worst measured expansion ratio automatically — nothing to configure, just upgrade for the allocation fix.
This is the foundation
HTTP content-coding and image decoding are exactly the kind of “boring, everywhere” I/O that either quietly stays Pure Rust or quietly doesn’t. NumRS2, SciRS2, ToRSh, OxiGeo, OxiMedia, VoiRS, TrustFormers, and SkLearS all pin oxiarc-* crates for archive, compression, and now HTTP/image I/O.
Repository: https://github.com/cool-japan/oxiarc
Star the repo if you’d rather your dependency tree said “Pure Rust” than “Pure Rust, except for this one gzip feature flag.”
The era of shipping a C decoder because the convenient feature flag defaulted to it is over. Pure Rust archiving, compression, HTTP, and images — fast, safe, and sovereign — is here.
— KitaSan at COOLJAPAN OÜ September 12, 2026