rust-gzip#
A GNU gzip-compatible compression tool written in Rust.
Passes 30/30 tests from the upstream GNU gzip 1.14 test suite, run unmodified against the rust-gzip binary in a Nix sandbox.
Usage#
The binary is gzip from pkgs.rust-gzip (release) or
pkgs.rust-gzip-dev (debug, faster compile). gunzip and zcat are
installed as symlinks.
# Compress / decompress
echo "hello" | gzip | gunzip
# From a Nix shell
nix run .#rust-gzip -- -d file.gz
Features#
- Compress / decompress / test / list with stdin, single file, multiple
files, and directory recursion (
-r). - Suffix handling:
-S <suffix>/--suffix, default.gz, canonical alternates (.tgz,.z,.Z,-gz,-z,_z). - Exit-code semantics: 0 success, 2 warning (out-of-range mtime), 1 hard error. Multi-file runs keep the worst code.
--help/--versionwrite to stdout; exit 1 on write failure (e.g./dev/full), matching coreutils conventions.--synchronous,---presume-input-ttyaccepted as no-ops.- Multi-member gzip streams; trailing NUL padding silently tolerated.
- Legacy format decompression:
- Pack (magic
1f 1e) — static Huffman, ported fromunpack.c. - LZW / compress (magic
1f 9d) — variable-width codes, ported fromunlzw.c.
- Pack (magic
Architecture#
Multi-module src/ layout:
| Module | Purpose |
|---|---|
main.rs |
Entry point |
cli.rs |
Argument parsing, Mode / Options, --help / --version |
compress.rs |
Gzip compression with hand-rolled framing (OS=3, exact mtime) |
decompress.rs |
Multi-format decompression (gzip, pack, LZW) with CRC32 + ISIZE trailer verification |
ops.rs |
File operations — compress / decompress / test / list for files and stdio |
unpack.rs |
Pack format decoder (magic 1f 1e, static Huffman) |
unlzw.rs |
LZW / compress decoder (magic 1f 9d, variable-width codes) |
util.rs |
CountingReader, suffix handling, output file creation |
Implementation notes#
- zlib-rs backend — flate2 is configured with the
zlib-rsfeature (pure Rust zlib rewrite) instead of the default miniz_oxide. This gives deflate validation strictness matching GNU gzip's own inflate (e.g. rejecting corrupt bitstreams that miniz_oxide silently accepts) while keeping the dependency tree C-free. - Hand-rolled gzip framing —
compress_streamwrites the 10-byte header directly (OS=3, source mtime or 0), delegates deflate toflate2::write::DeflateEncoder, then appends the CRC32 + ISIZE trailer. This byte-exact control is what passes thereferenceandreproducibletests. - Member-by-member decoding —
decompress_streambuffers the full input and walks member boundaries usingflate2::Decompresswith a flag-aware header parser, rather than usingMultiGzDecoder. This handles multi-member archives, trailing NUL padding, legacy format dispatch, and-cdfcat-style pass-through. - Deflate no-progress → format violated — when zlib-rs consumes all
input without reaching
StreamEnd, the error is reported asinvalid compressed data--format violatedto match GNU gzip's owninflate.cbehavior (structural Huffman errors, not premature EOF).
Testing#
Each of the 30 upstream test scripts runs as its own Nix check:
# Single test
nix build .#checks.x86_64-linux.rust-gzip-test-keep
# View a failure log
nix log .#checks.x86_64-linux.rust-gzip-test-keep
# All tests (keep going on failures)
nix build .#checks.x86_64-linux.rust-gzip-test-* --keep-going --no-link
Test harness#
The harness (testsuite.nix) extracts pkgs.gzip.src, builds a shadow
$PATH with gzip / gunzip / zcat pointing at rust-gzip-dev and
companion scripts (zdiff, zgrep, znew, …) from the upstream tarball,
exports the environment variables the gnulib init.sh framework expects
(LC_ALL=C, srcdir, VERSION, etc.), then runs each test script and
propagates its exit code. Exit 77 (automake "skip") is treated as pass.