fix(cli): reject -j 0 instead of silently falling through to serial

`--threads` was an `Option<usize>` with no value-parser bound. Passing
`-j 0` slipped past clap and reached the dispatch in `crypto::encrypt`
/ `decrypt`, where the `threads > 1` check evaluates to false for 0 and
the call quietly fell through to the serial path. The user thought
they had asked for "no worker threads" and got something else instead;
either way, 0 workers is not a meaningful configuration.

Switch the field to `Option<u32>` and add `value_parser!(u32).range(1..)`
so clap rejects `-j 0` at parse time with a usage error. Cast to
`usize` at the single use site. Using `u32` rather than `usize` avoids
shipping a host-pointer-width-dependent CLI surface; thread counts well
past 4 billion are not a thing we need to plan for.

Test plan:
  - New integration test `rejects_zero_threads` invokes the binary
    with `-j 0` and asserts non-zero exit.
  - Existing `roundtrip_multi_threaded` (`-j 4`) still passes, so the
    range bound has not broken normal usage.

Refs: external review (GLM51 #1; Gemini #3 misdescribed the symptom
as "silent corruption" — verified the actual behaviour was a fall-
through to the serial path, not output corruption, but the fix is
the same).
This commit is contained in:
2026-05-02 21:29:28 +02:00
parent 91b459657e
commit 2c101abdbd
2 changed files with 24 additions and 3 deletions
+3 -3
View File
@@ -67,8 +67,8 @@ struct Cli {
/// Number of worker threads for AEAD work. Defaults to the number of
/// available CPUs. Set to 1 for fully serial encrypt/decrypt.
#[clap(short = 'j', long)]
threads: Option<usize>,
#[clap(short = 'j', long, value_parser = clap::value_parser!(u32).range(1..))]
threads: Option<u32>,
/// Random-access decrypt: byte offset of the slice to read.
/// Requires `--decrypt`, an `--input-file` whose header has the
@@ -175,7 +175,7 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
let argon_memory = cli.argon_memory;
let argon_passes = cli.argon_passes;
let argon_parallelism = cli.argon_parallelism;
let threads = cli.threads.unwrap_or_else(|| {
let threads = cli.threads.map(|n| n as usize).unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)