zstd-complete 0.1.0

A pure Rust Zstandard compressor and decompressor with bounded streaming I/O
Documentation

zstd-complete

crates.io API documentation CI

zstd-complete is a pure-Rust Zstandard compressor and decompressor. It supports bounded streaming compression, all standard positive compression levels, dictionaries, checksums, portable no_std operation, and safe public interfaces.

“Complete” means that the crate provides both sides of the Zstandard format. It does not mean that it reproduces the zstd C API or every advanced feature of the C library. The exact differences are listed below.

The project is a fork of ruzstd. Its high-performance compressor was derived from and translated from zstd C 1.5.7, then substantially reworked around Rust ownership, errors, and streaming. Format interoperability is a requirement; byte-for-byte identity with C output is not.

Install

[dependencies]
zstd-complete = "0.1"

The package name contains a hyphen, so Rust code imports it as zstd_complete.

The default features are std and hash. Disable default features for the lower-level no_std compressor and decompressor. Enable dict_builder to train raw-content dictionaries.

Bounded streaming compression

Encoder<W> implements std::io::Write and reports configuration, memory budget, and underlying I/O failures. It defaults to level 3, 8 MiB input frames, and a conservative 96 MiB working-memory budget.

# #[cfg(feature = "std")]
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::{fs::File, io::Write};
use zstd_complete::encoding::{CompressionLevel, Encoder, EncoderOptions};

let input = b"data can be supplied in any number of writes";
let output = File::create("archive.zst")?;
let options = EncoderOptions::new(CompressionLevel::DEFAULT)
    .with_checksum(true);
let mut encoder = Encoder::new(output, options)?;
encoder.write_all(input)?;
encoder.finish()?;
# Ok(())
# }
# #[cfg(not(feature = "std"))]
# fn main() {}

Each configured input chunk is emitted as an independent Zstandard frame. Concatenated frames are valid Zstandard and keep memory bounded regardless of total input size. Smaller chunks can reduce compression ratio because matches do not cross frame boundaries. flush() closes the current frame; call finish() to write the final frame and recover the wrapped writer.

For small inputs, encode streams from a reader to a writer and encode_all returns a Vec<u8>. The older compress, compress_to_vec, and FrameCompressor interfaces remain available, including in no_std, but the bounded, fallible API is preferred for new std applications.

Opt-in parallel compression

Enable the non-default multithreading feature to compress independent frames on multiple worker threads. ParallelEncoder implements Write, preserves frame order, bounds the number of in-flight frames, and applies the configured memory limit to the aggregate worker estimate. A worker count of one delegates directly to the existing Encoder; the default single-threaded implementation does not contain channels, locks, atomics, or worker checks in its compression path.

# #[cfg(all(feature = "std", feature = "multithreading"))]
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::{io::Write, num::NonZeroUsize};
use zstd_complete::encoding::{CompressionLevel, EncoderOptions, ParallelEncoder};

let options = EncoderOptions::new(CompressionLevel::DEFAULT)
    .with_memory_limit(256 * 1024 * 1024);
let mut encoder =
    ParallelEncoder::new(Vec::new(), options, NonZeroUsize::new(4).unwrap())?;
encoder.write_all(b"input split into ordered independent frames")?;
let compressed = encoder.finish()?;
# let _ = compressed;
# Ok(())
# }
# #[cfg(not(all(feature = "std", feature = "multithreading")))]
# fn main() {}

Parallelism begins only when at least two complete input frames are available, so inputs no larger than one configured frame do not gain throughput. Smaller frame chunks expose more parallel work but may reduce compression ratio because matches never cross frame boundaries.

Decompression

StreamingDecoder implements the crate's Read interface for one frame:

# #[cfg(feature = "std")]
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::{fs::File, io::Read};
use zstd_complete::decoding::StreamingDecoder;

let input = File::open("archive.zst")?;
let mut decoder = StreamingDecoder::new(input)?;
let mut decoded = Vec::new();
decoder.read_to_end(&mut decoded)?;
# Ok(())
# }
# #[cfg(not(feature = "std"))]
# fn main() {}

Use FrameDecoder when you need incremental buffer collection, dictionary selection, explicit block limits, concatenated-frame handling, or control over skippable frames. StreamingDecoder currently stops after one frame; this is important when consuming the multi-frame output of a bounded Encoder.

Coverage compared with zstd C 1.5.7

Capability zstd-complete 0.1 Notes
Zstandard compression Yes Standard levels 1 through 22 plus uncompressed frames
Zstandard decompression Yes Raw, RLE, compressed blocks, checksums, and dictionaries
Raw-content and formatted dictionaries Yes Reusable prepared encoder dictionaries and decoder dictionaries
Bounded compression of arbitrarily large input Yes Emits independent frames at configurable chunk boundaries
no_std with alloc Yes Lower-level encode/decode APIs; the std::io encoder requires std
Portable scalar implementation Yes Used on non-x86 targets and available as a forced release gate
Runtime-selected x86-64 acceleration Yes BMI2 paths are selected at runtime; distributed binaries need not use target-cpu=native
Multithreaded compression Opt-in The multithreading feature parallelizes ordered independent frames
zstd C ABI/API compatibility No This is an idiomatic Rust API, not a drop-in C replacement
Full advanced parameter surface No Negative levels, worker controls, pledged-size tuning, and targetCBlockSize are not stable public options
Stable byte identity with zstd C No Output is format-compatible and can legitimately differ in block and match choices

The compressor implements the nine zstd strategy families used by the positive level table: Fast, DFast, Greedy, Lazy, Lazy2, BTLazy2, BTOpt, BTUltra, and BTUltra2. The public API intentionally exposes validated CompressionLevel values rather than C-style numeric tuning controls.

Interoperability tests cover Rust-produced output decoded by zstd C, C-produced fixtures decoded by Rust, checksummed frames, raw blocks, concatenated frames, and raw and formatted dictionaries. The retained differential matrix has 1,095 normal, target-size, and prepared-dictionary rows across 73 real-world fixtures; all rows decode. This is strong regression coverage, not a claim that every possible Zstandard stream has been exhaustively tested.

The release gate also compressed 35 files selected from the official zstd 1.5.7 source tree at levels 1, 3, 5, 8, 16, 19, and 22. All 245 Rust archives decoded with zstd C and reproduced their inputs. Rust also reproduced zstd C's decoded output for the four official golden decompression frames, including empty, RLE-first, zero-sequence, and 128 KiB block cases.

Safety

The normal public API is safe Rust: callers do not need an unsafe block. Performance-critical internals do use unsafe for bounded unaligned loads, initialized-buffer construction, entropy emission, and the decoder ring buffer. Those modules are private, their call-site invariants are documented, and portable scalar implementations remain available.

For the 0.1 release candidate, the local gate passed 758 tests (5 diagnostic tests ignored), strict Clippy, rustdoc, no_std wasm checks, forced-scalar tests, packaged-source compilation, 353 focused AddressSanitizer tests, and focused Miri checks of the ring buffer, short writes, unaligned loads, and prepared-sequence entropy path. Bounded ASan/libFuzzer campaigns completed 2,644 decoder cases and 424 public-encoder cases; encoder cases round-tripped through both Rust and zstd C. CI also defines native Windows and Apple-Silicon jobs. These checks are evidence supporting memory safety; they are not a formal proof or a warranty.

The translated compressor has received extensive differential testing, interoperability testing, and repeated hardware-counter benchmarking. It has not received an independent human line-by-line source review. This is disclosed so adopters can apply their own assurance requirements.

Benchmarks against zstd C

The primary real-world benchmark is the standard 211,938,580-byte Silesia corpus: 12 individually compressed text, executable, source, database, XML, and medical-image files ranging from 5 MiB to 51 MiB. Each implementation compressed every file three times in each of three perf stat samples, with input resident in memory and an owned compressed buffer returned. Rust used the public bounded API and its default 8 MiB frames; C 1.5.7 used ZSTD_compress2() and one frame per file.

Level Rust bytes C bytes Size gap Rust MB/s C MB/s Rust instructions/corpus C instructions/corpus Instruction gap
1 73,259,577 73,276,937 -0.024% 295.1 403.2 6.029 B 5.024 B +19.99%
3 66,232,654 66,216,569 +0.024% 212.3 240.0 7.204 B 6.619 B +8.83%
8 59,930,104 59,820,035 +0.184% 59.5 69.4 35.041 B 32.854 B +6.66%

Negative size gaps favor Rust. The result shows near-identical aggregate compression ratio and a remaining C speed advantage, especially at level 1. Every Rust archive was accepted by zstd C, and Rust decoded and byte-verified all 12 C level-3 archives. Corpus provenance, hashes, exact methodology, cycles, the seven-level official-source matrix, and reproduction commands are in RELEASE_BENCHMARKS_2026-08-30.md.

The smaller generated-shape benchmark below exposes cases that a mixed corpus can hide, particularly incompressible and all-zero data.

The public bounded encoder was tested first. Each generated input is 16 MiB; Rust used its default two 8 MiB frames while the C one-shot API used one frame. Each program compressed the input three times, and each counter is the mean of three perf stat runs. Every Rust archive passed zstd -t with zstd C 1.5.7.

Level-3 input Rust bytes C bytes Rust instructions/run C instructions/run Rust instruction gap
Deterministic random 16,777,620 16,777,610 54.58 M 45.10 M +21.02%
Long-distance repetition 8,390,996 8,390,960 85.71 M 77.12 M +11.14%
Repeated records 1,706 1,621 44.78 M 39.78 M +12.56%
Structured JSONL 1,337,412 1,340,481 239.83 M 221.22 M +8.41%
All zeros 550 531 133.07 M 45.44 M +192.86%

This is the relevant end-user comparison, including the bounded encoder's frame-management cost and compression-ratio tradeoff. The all-zero case makes the largest remaining low-entropy overhead especially clear. Different frame counts mean this is a product-level comparison, not isolated algorithm parity.

The following narrower benchmark compares the retained one-shot compressor core with C under matching framing. It is useful for CPU attribution across strategy levels, but should not be substituted for the public-API table above.

The table below is the final release snapshot from 2026-08-30 on an AMD Ryzen 7 3700X, Linux x86-64, rustc 1.94.1. Both implementations compressed the same 1,022,035-byte real-world fixture 20 times in one process; each counter is the mean of three perf stat runs. Rust was built with --release and one code-generation unit. The C comparison is zstd 1.5.7 through ZSTD_compress2.

Level Rust bytes/run C bytes/run Rust instructions/run C instructions/run Rust instruction gap
1 571,525 571,525 27.34 M 25.32 M +8.00%
3 498,891 498,911 46.75 M 42.56 M +9.85%
5 488,363 489,007 100.68 M 110.35 M -8.76%
8 484,336 484,336 138.19 M 145.35 M -4.93%
16 460,555 460,806 393.91 M 435.47 M -9.54%

Negative gaps favor Rust. Instruction counts are used because wall-clock cycles are more sensitive to frequency and scheduling. This focused fixture shows where the remaining low-level CPU gap lies, but it is not a general throughput claim. The broader corpus, exact commands, output-size comparisons, upstream ruzstd comparison, streaming ratio measurements, and known limitations are in C_PORT_PAUSE_REPORT_2026-08-29.md and RELEASE_READINESS.md.

For historical context, on the same level-1 fixture the pre-port upstream ruzstd 0.9.1 Fastest path produced 619,101 bytes/run and used about 128.69 million instructions/run. This release produces 571,525 bytes/run and uses about 27.34 million instructions/run. That comparison covers level 1 only; upstream did not contain this release's complete C level table.

Building for performance

Normal Cargo release builds are supported. The benchmarked configuration uses one code-generation unit in the final application:

[profile.release]
codegen-units = 1

Cargo profiles belong to the final application, so this repository's setting does not automatically apply to downstream users. The optimization is optional: the external-consumer benchmark also showed a large advantage over upstream under Cargo's default release profile.

Do not set target-cpu=native for binaries distributed to other machines. Optimized x86-64 kernels use runtime feature detection, while scalar paths cover x86-64 without BMI2, AArch64, Windows, Apple platforms, and wasm no_std.

Dictionary training

Enable dict_builder for the dictionary module's raw-content dictionary trainer. It does not generate formatted dictionaries containing entropy tables. On the historical github-users sample set, its output was within 0.2% of the official trainer's compression result. Existing formatted dictionaries can still be consumed by the encoder and decoder.

Licensing and provenance

This package is distributed under BSD-3-Clause, selecting the BSD option offered by the Zstandard reference implementation.

This project is a fork of KillingSpark/zstd-rs, created by Moritz Borcherding and developed by its contributors. Substantial inherited Rust remains in the decoder and supporting format, entropy, dictionary, and I/O code. Its MIT copyright and permission notice is preserved as required attribution in LICENSES/ruzstd-MIT.txt; it is not a second outbound license choice for this package. See THIRD_PARTY_NOTICES.md for credit, provenance, and the detailed compressor source map.

The implementation is not represented as clean-room work and is not intended to remain a faithful C port. Future maintenance will improve the Rust design and performance directly while preserving Zstandard format interoperability; recurring audits of future zstd C releases are not part of that model.