The Rust CLI frontend rejected -r, --filelist, --output-dir-flat,
--output-dir-mirror, -l and --list, which tests/playTests.sh exercises
heavily (32 recursive runs, 20 file-list runs, 32 output-dir runs, 29
list runs). These are pure parser/dispatch features: the actual
directory traversal, file-of-names reading, and frame inspection remain
in C.
Mirror the pre-migration C zstdcli.c control flow:
- --filelist names are collected during parsing and expanded after the
symbolic-link filter through UTIL_createFileNamesTable_fromFileName,
so list contents are never symlink-filtered and an unreadable,
irregular, empty, or over-long list aborts with "error reading NAME"
and exit code 1, exactly like the C loop.
- -r expands the collected inputs through UTIL_createExpandedFNT,
following links only under --force (the C followLinks flag). The
input-name count is saved before expansion: when expansion leaves no
files but names were given (only empty directories), the run prints
"please provide correct input file(s) or non-empty directories --
ignored" and exits 0 instead of falling back to stdin. -r parses only
on unix/windows targets, mirroring UTIL_HAS_CREATEFILELIST.
- --output-dir-flat / --output-dir-mirror pass through to the existing
output-dir arguments of FIO_{,de}compressMultipleFilenames (test mode
included). Both use exact NEXT_FIELD semantics via a new next_field
helper: an attached empty value (--output-dir-flat=) stays empty and
is rejected with the C "output dir cannot be empty string" message
instead of consuming the next argument. --output-dir-mirror parses
only on POSIX targets, mirroring UTIL_HAS_MIRRORFILELIST.
- -l/--list dispatches to FIO_listMultipleFiles after expansion and
before the console checks, so "No files given", the stdin refusal,
display-level gating, and per-file exit codes are the C code paths.
Builds without the decompression feature report "file information is
not supported" like ZSTD_NODECOMPRESS builds.
- g_utilDisplayLevel is published before any expansion so traversal
warnings honor -q/-v, and benchmark dispatch moved after expansion so
-rqi0b1e2 style aggregates benchmark expanded directories like C.
FIO_setNbFilesTotal keeps receiving the post-expansion count because
apply_preferences reads cli.inputs after all expansion steps.
The FileNamesTable struct is mirrored in Rust (pointer/pointer/size/
size); tables returned by the UTIL helpers are drained into owned
CStrings and freed immediately, so no C buffer lifetime escapes.
Test Plan:
- cd rust/cli && cargo clippy --all-targets -- -D warnings && cargo
test --all-targets, plus the compression-only and decompression-only
feature matrices; new parser tests cover -r aggregation, both
--filelist syntaxes, option-as-argument rejection, empty output-dir
rejection, --list forms, and --list=x rejection.
- make -C programs zstd, then diffed behavior against a pre-migration
C binary (f8745da6): -r on a nested tree produces identical file
sets and identical -o output; --filelist with one and two lists,
missing list ("zstd: error reading X", exit 1), and binary-garbage
list (exit 1); --output-dir-flat compress + decompress round-trip;
--output-dir-mirror round-trip and '..' rejection; -l/-lv identical
stdout, identical exit codes for no-files, non-zst, and stdin cases;
-r on an empty directory exits 0 with the same message; -c -r with
stdin fallback produces identical frames.
Rust rewrite
This directory contains the in-progress Rust replacement for the zstd library and command-line program. During the migration, the crate is built as a static library and linked into the original C test programs. Production C translation units become declaration-only shims as their implementations move to Rust; the original C tests remain unchanged and provide compatibility coverage.
Component map
The crate is organized from low-level representation helpers toward the public zstd ABI:
- Common primitives
mem,bits,bitstream, andcpuimplement byte-order, bitstream, and target-feature operations used by the codecs.errors,debug,xxhash, andzstd_commonprovide common exported ABI functions and state.commoncontains shared frame constants and internal data types.
- Entropy coding
entropy_commonreads FSE normalized counts and Huffman statistics.fse_decompressbuilds FSE decoding tables and decodes FSE streams.fse_compressnormalizes counts, writes FSE headers, builds compression tables, and encodes FSE streams.huf_compressbuilds Huffman compression tables, writes table headers, and encodes one- and four-stream Huffman payloads.huf_decompressbuilds Huffman decoding tables and decodes X1 and X2 Huffman streams.
- Compression primitives
histcounts byte frequencies for FSE and Huffman compression.zstd_presplitchooses split points for full compression blocks.zstd_compress_literalsemits raw, RLE, and Huffman literal sections while preserving the compressor's Huffman-table repeat state.zstd_compress_frameserializes frame headers, skippable frames, and the last empty block; it takes scalar frame parameters so the C-ownedZSTD_CCtx_paramslayout never crosses the language boundary.zstd_compress_paramsowns the compression-level tables (formerlyclevels.h), parameter bounds, clamping, validation, table selection, source/dictionary adjustment, and match-state/CDict size estimation. The C integration layer keeps the publicZSTD_*symbols and feeds the leaves configuration-owned scalars: the excluded-block-compressor strategy cascade, struct sizes, and sanitizer redzone policy.zstd_fastandzstd_double_fastimplement the single- and two-table fast block match finders, including attached and external dictionary paths.zstd_lazyimplements greedy, lazy, lazy2, and binary-tree matching, including row-based and dictionary search variants.zstd_opt_treemaintains the binary-tree index used by optimal matching; the dynamic-programming optimal parser itself remains in C for now.zstd_ldmimplements long-distance-match parameter selection, table maintenance, sequence generation, and sequence consumption.
- Dictionary building
divsufsortconstructs the suffix array that drives the legacyZDICTtrainer (ZDICT_trainFromBuffer_legacy). The sample analysis and dictionary assembly inzdict.c,cover.c, andfastcover.cremain C.
- Runtime support
threadingprovides platform pthread wrappers required by zstd headers.poolimplements the bounded worker pool used by multithreaded compression.
- Dictionary support
zstd_ddictowns, loads, copies, and references decode dictionaries.
- Block decompression
zstd_decompress_blockdecodes literal and sequence sections, maintains FSE/Huffman repeat state, and executes compressed-block sequences.zstd_decompressowns the public decompression context, one-shot, dictionary, parameter, and streaming state machines. Its C shim retains configuration-dependent context allocation plus legacy and trace leaves.
- Command-line frontend
zstd_cliowns the Rust parser, safety policy, and dispatch. It is built by the separatecli/static-library package only for program archives, so library builds do not acquire program-only dependencies. The Cfileiobackend still owns file opening, safe replacement, sparse writes, metadata, and streaming I/O.timefnprovides the monotonic nanosecond clock behindUTIL_time_t, andbenchfnowns the benchmark run/timing loop (BMK_benchFunction,BMK_benchTimedFn) used by the CLI benchmark mode and by C test tools. Both live in thecli/package, but C test binaries (fullbench, fuzzer, zstreamtest, paramgrill, ...) link a helpers-only build of that archive, produced without the package'sclifeature, because the parser layer requires the Cfileiobackend that tests do not compile. Benchmark orchestration and reporting (benchzstd.c) remain C, reached from the Rust parser through theZSTD_NOBENCH-gated bridge inzstdcli.c.
The optimal block matcher, high-level frame compression, dictionary-building
except suffix-array construction, legacy decoding callbacks, benchmark
orchestration (benchzstd), and the CLI
file-I/O backend are still C. They must move before the rewrite is complete.
Keeping that boundary explicit prevents a passing hybrid build from being
mistaken for the final all-Rust result.
Compatibility boundary
The public ABI continues to come from the existing headers under lib/.
Exported Rust functions therefore use C layout and calling conventions. A C
source file whose implementation has moved to Rust remains in the original
makefile source list as a small shim so header configuration and platform
preprocessor behavior stay available during the transition.
The library, test, and program makefiles select an archive directory for the active C configuration: enabled compression/decompression/dictionary-builder modules, default or forced HUF X1/X2, and the matching Rust target for 32-bit C binaries. The native static archive flattens Rust object members rather than nesting a Rust archive, while the native shared library retains all migrated Rust exports. When the HUF mode changes, the test and program paths also rebuild cached C outputs before linking. This prevents original C tests from using a stale or configuration-incompatible implementation.
Validation
Run focused Rust checks from this directory:
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test --all-targets
cargo build --release
The program-only Rust archive has its own feature matrix and should be checked
from rust/cli as well:
cargo clippy --all-targets -- -D warnings
cargo test --all-targets
cargo test --no-default-features --features cli,compression --all-targets
cargo test --no-default-features --features cli,decompression --all-targets
cargo test --no-default-features --all-targets
Then run original compatibility tests from the repository root, starting with the narrow target for the component being migrated. For example:
make -C tests fuzzer
./tests/fuzzer -i1 --no-big-tests
make -C tests test-rust-lib-smoke
Broader tests/Makefile targets remain the authoritative integration gates as
more of the library and CLI are rewritten.