feat(cli): move multi-file output policy to Rust

Port the scalar decision matrix for multi-input concatenation behind the CLI Rust ABI. C retains warnings, preference mutation, fatal diagnostics, and confirmation prompts; Rust now classifies protection, remove-file disabling, quiet abort, and confirmation paths with focused coverage.

Test Plan:

- cargo test --manifest-path rust/cli/Cargo.toml fileio_prefs::tests::multi_files_concat

- cargo clippy --manifest-path rust/cli/Cargo.toml

- cargo clippy --manifest-path rust/cli/Cargo.toml --benches

- cargo clippy --manifest-path rust/cli/Cargo.toml --tests

- cargo +nightly fmt --manifest-path rust/Cargo.toml --all

- make -B -C programs -j2 zstd

- make -C tests -j2 test-cli-tests
This commit is contained in:
2026-07-18 06:38:24 +02:00
parent 21f8e8958c
commit 684963c1a9
2 changed files with 176 additions and 22 deletions
+43 -22
View File
@@ -286,6 +286,23 @@ typedef char FIO_rust_ctx_size[
int FIO_rust_shouldDisplayFileSummary(const FIO_ctx_t* fCtx);
int FIO_rust_shouldDisplayMultipleFileSummary(const FIO_ctx_t* fCtx);
enum {
FIO_RUST_MULTI_FILES_ACTION_PROCEED = 0,
FIO_RUST_MULTI_FILES_ACTION_FATAL_STDOUT_REMOVE = 1,
FIO_RUST_MULTI_FILES_ACTION_FATAL_TEST_REMOVE = 2,
FIO_RUST_MULTI_FILES_ACTION_DISABLE_REMOVE = 3,
FIO_RUST_MULTI_FILES_ACTION_QUIET_ABORT = 4,
FIO_RUST_MULTI_FILES_ACTION_CONFIRM = 5
};
int FIO_rust_multiFilesConcatAction(int nbFilesTotal,
int hasStdoutOutput,
int testMode,
int hasOutputFile,
int removeSrcFile,
int overwrite,
int displayLevel,
int displayLevelCutoff);
static int FIO_shouldDisplayFileSummary(FIO_ctx_t const* fCtx)
{
return FIO_rust_shouldDisplayFileSummary(fCtx);
@@ -761,28 +778,28 @@ static void FIO_adjustMemLimitForPatchFromMode(FIO_prefs_t* const prefs,
*/
static int FIO_multiFilesConcatWarning(const FIO_ctx_t* fCtx, FIO_prefs_t* prefs, const char* outFileName, int displayLevelCutoff)
{
if (fCtx->hasStdoutOutput) {
if (prefs->removeSrcFile)
/* this should not happen ; hard fail, to protect user's data
* note: this should rather be an assert(), but we want to be certain that user's data will not be wiped out in case it nonetheless happen */
EXM_THROW(43, "It's not allowed to remove input files when processed output is piped to stdout. "
"This scenario is not supposed to be possible. "
"This is a programming error. File an issue for it to be fixed.");
}
if (prefs->testMode) {
if (prefs->removeSrcFile)
/* this should not happen ; hard fail, to protect user's data
* note: this should rather be an assert(), but we want to be certain that user's data will not be wiped out in case it nonetheless happen */
EXM_THROW(43, "Test mode shall not remove input files! "
"This scenario is not supposed to be possible. "
"This is a programming error. File an issue for it to be fixed.");
return 0;
}
int action = FIO_rust_multiFilesConcatAction(
fCtx->nbFilesTotal, fCtx->hasStdoutOutput, prefs->testMode,
outFileName != NULL, prefs->removeSrcFile, prefs->overwrite,
g_display_prefs.displayLevel, displayLevelCutoff);
if (action == FIO_RUST_MULTI_FILES_ACTION_FATAL_STDOUT_REMOVE)
/* this should not happen ; hard fail, to protect user's data
* note: this should rather be an assert(), but we want to be certain that user's data will not be wiped out in case it nonetheless happen */
EXM_THROW(43, "It's not allowed to remove input files when processed output is piped to stdout. "
"This scenario is not supposed to be possible. "
"This is a programming error. File an issue for it to be fixed.");
if (action == FIO_RUST_MULTI_FILES_ACTION_FATAL_TEST_REMOVE)
/* this should not happen ; hard fail, to protect user's data
* note: this should rather be an assert(), but we want to be certain that user's data will not be wiped out in case it nonetheless happen */
EXM_THROW(43, "Test mode shall not remove input files! "
"This scenario is not supposed to be possible. "
"This is a programming error. File an issue for it to be fixed.");
if (fCtx->nbFilesTotal == 1) return 0;
assert(fCtx->nbFilesTotal > 1);
if (!outFileName) return 0;
if (!outFileName || prefs->testMode) return 0;
if (fCtx->hasStdoutOutput) {
DISPLAYLEVEL(2, "zstd: WARNING: all input files will be processed and concatenated into stdout. \n");
@@ -792,22 +809,26 @@ static int FIO_multiFilesConcatWarning(const FIO_ctx_t* fCtx, FIO_prefs_t* prefs
DISPLAYLEVEL(2, "The concatenated output CANNOT regenerate original file names nor directory structure. \n")
/* multi-input into single output : --rm is not allowed */
if (prefs->removeSrcFile) {
if (action == FIO_RUST_MULTI_FILES_ACTION_DISABLE_REMOVE) {
DISPLAYLEVEL(2, "Since it's a destructive operation, input files will not be removed. \n");
prefs->removeSrcFile = 0;
action = FIO_rust_multiFilesConcatAction(
fCtx->nbFilesTotal, fCtx->hasStdoutOutput, prefs->testMode,
1, 0, prefs->overwrite, g_display_prefs.displayLevel,
displayLevelCutoff);
}
if (fCtx->hasStdoutOutput) return 0;
if (prefs->overwrite) return 0;
if (action == FIO_RUST_MULTI_FILES_ACTION_PROCEED) return 0;
/* multiple files concatenated into single destination file using -o without -f */
if (g_display_prefs.displayLevel <= displayLevelCutoff) {
if (action == FIO_RUST_MULTI_FILES_ACTION_QUIET_ABORT) {
/* quiet mode => no prompt => fail automatically */
DISPLAYLEVEL(1, "Concatenating multiple processed inputs into a single output loses file metadata. \n");
DISPLAYLEVEL(1, "Aborting. \n");
return 1;
}
/* normal mode => prompt */
assert(action == FIO_RUST_MULTI_FILES_ACTION_CONFIRM);
return UTIL_requireUserConfirmation("Proceed? (y/n): ", "Aborting...", "yY", fCtx->hasStdinInput);
}