Skip to content

feat(cli): #28 Phase 6.3 — zstd CLI binary with full upstream v1.5.7 parity #128

@polaz

Description

@polaz

Summary

Phase 6.3 of #28: ship a zstd CLI binary with full upstream v1.5.7 command-line parity. Single binary; downstream packagers create the conventional symlinks (zstdcat, unzstd, zstdmt) and our binary detects them via argv[0] and adjusts behaviour, matching upstream's dispatch.

Deliverables

1. New workspace member cli/ (binary crate)

[package]
name = "structured-zstd-cli"
# ...

[[bin]]
name = "zstd"
path = "src/main.rs"

Default-builds an optimized binary; downstream packagers strip + install to /usr/bin/zstd and ln -s zstd /usr/bin/{zstdcat,unzstd,zstdmt,zstdgrep,zstdless}.

2. CLI dispatch by argv[0]

Upstream behaviour replicated:

  • zstd: default — compresses by default
  • zstdcat / zstdcat.exe: decompresses to stdout
  • unzstd: decompresses, output to file
  • zstdmt: compresses with multi-threading (-T0 default, max threads)
  • zstdgrep: decompresses + pipes to grep
  • zstdless: decompresses + pipes to less (sets LESSOPEN)

3. Command-line surface

Full parity with upstream zstd --help output. Argument parser uses clap v4 with custom builder mode (not derive — upstream's help text is hand-formatted and we need byte-similar output for --help/--version).

Mode flags:

  • -z, --compress (default unless argv[0] says otherwise)
  • -d, --decompress, --uncompress
  • -t, --test
  • -l, --list
  • -b, --benchmark (with -b#, -e#, -i#, -B#, -S follow-ups)

Compression level:

I/O:

  • -c, --stdout, --to-stdout
  • -o FILE, --output FILE
  • -f, --force
  • -k, --keep
  • --rm
  • --no-progress
  • Reading from stdin when no FILE arg (and stdin is not a TTY) or with -
  • Output suffix: .zst (compress), strip .zst (decompress)

Dictionary:

  • -D dictfile, --use-dict=FILE
  • --train, --train-fastcover, --maxdict=N, --dictID=N

Verbosity:

  • -q, --quiet
  • -v, --verbose
  • -vv, -vvv (extra debug)
  • --version, -V
  • --help, -h, -H (long help)

Multi-thread (delegated to 6.4):

  • -T#, --threads=N — handled here as parsing + forwarding to ZSTD_c_nbWorkers. Actual MT impl lives in 6.4 (chore: release v0.0.7 #64).
  • --single-thread, --adapt, --adapt=min=N,max=N

Format toggles (for non-zstd outputs upstream's CLI also handles):

  • --format=zstd / gzip / xz / lz4 — this issue implements zstd only; the other formats are deferred to a future issue (out of scope for drop-in since Alpine zstd package's gzip/xz/lz4 modes are optional).

Environment variables:

  • ZSTD_CLEVEL — default compression level
  • ZSTD_NBTHREADS — default thread count
  • ZSTD_NB_WORKERS — alias for ZSTD_NBTHREADS

4. File operations

  • Read regular file, stat preserved (mtime, mode propagated to output on --keep).
  • Multi-file mode: zstd a b c compresses each individually to a.zst, b.zst, c.zst.
  • Glob-like: not handled by us (shell expands).
  • Output to - means stdout regardless of where positional args resolve.
  • Error on existing output without -f; refuses to write compressed to a TTY without -f.
  • --rm deletes source after successful operation.

5. Help/version output matching

--version output: zstd version 1.5.7 (structured-zstd v0.0.X) — first line matches upstream prefix, second line identifies our impl. --help body matches upstream text where applicable (sections, flag descriptions). Where our impl diverges from upstream behaviour (e.g. no MT in 6.3, no legacy decode in 6.3), the help text spells that out explicitly so users aren't surprised.

6. Tests (cli/tests/)

  • cli/tests/integration.rs — drives the binary via assert_cmd for happy-path scenarios (compress/decompress/test/list, stdin/stdout, multi-file).
  • cli/tests/argv0_dispatch.rs — symlinks target/debug/zstd to zstdcat/unzstd/zstdmt and verifies the dispatch.
  • cli/tests/help_snapshot.rs — snapshot --help output so accidental changes show up in review.
  • cli/tests/env.rsZSTD_CLEVEL/ZSTD_NBTHREADS exercised.

Out of scope

Acceptance criteria

  • zstd FILE compresses to FILE.zst, removes source on --rm, default compression level matches upstream (= 3).
  • zstd -d FILE.zst decompresses to FILE.
  • cat FILE | zstd | zstd -d > FILE.out: byte-exact roundtrip via stdin/stdout.
  • zstd -t FILE.zst: silent success, non-zero exit on corruption.
  • zstd -l FILE.zst: emits frame metadata table matching upstream column layout for at least the fixed columns (frame ID, content size, ratio, check, filename).
  • zstdcat FILE.zst: decompresses to stdout regardless of TTY.
  • --version first line: zstd version 1.5.7 (structured-zstd vX.Y.Z).
  • ZSTD_CLEVEL=11 zstd FILE uses level 11.
  • --ultra unlocks -20..-22; without --ultra those flags error out.
  • -T0 forwards nbWorkers = max_threads; treated as 1 if 6.4 not merged.
  • cli/tests/ suite: 100% green under cargo nextest.

Estimate

~12-14 working days (~2000 LoC for CLI, ~500 LoC for tests, plus help-text snapshotting).

Blocked by

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2-mediumMedium priority — important improvementenhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions