feat(cli): move destination opening into Rust

FIO_openDstFile still performed destination classification, platform-specific
file creation, stdio buffering, and overwrite preparation in C, even though
source opening and removal already used Rust filesystem leaves. That made the
CLI backend's most important output safety path split across two implementations.

Add a Rust destination leaf with a small status ABI. Rust owns sentinel and
same-file classification, regular-file detection, platform binary open and
fdopen, truncation, and buffering. C keeps the user-facing diagnostics,
sparse-mode preference mutation, overwrite prompt, and existing remove-file
wrapper; it performs a second Rust open after an accepted overwrite decision so
no prompt or policy is duplicated. Existing file contents are left untouched
until that C-owned decision is complete.

Test Plan:
- `cargo test --manifest-path rust/Cargo.toml --lib -- --test-threads=1`
  -- passed (411 tests, including destination classification/open tests).
- `cargo clippy --manifest-path rust/Cargo.toml --lib -- -D warnings`
  -- passed.
- `cargo clippy --manifest-path rust/cli/Cargo.toml --lib
  --no-default-features --features cli,compression,decompression,benchmark,
  dict-builder -- -D warnings` -- passed.
- Nightly fmt checks for both Rust manifests -- passed.
- `make -B -C tests -j2 test-cli-tests` -- passed (41 tests).
This commit is contained in:
2026-07-18 18:44:12 +02:00
parent 7867f64413
commit 1b85c430b1
2 changed files with 428 additions and 85 deletions
+86 -80
View File
@@ -361,6 +361,23 @@ int FIO_rust_openSrcFile(int allowBlockDevices,
const char* srcFileName,
stat_t* statbuf,
FILE** outFile);
enum {
FIO_RUST_OPEN_DST_SUCCESS = 0,
FIO_RUST_OPEN_DST_SETBUF_FAILED = 1,
FIO_RUST_OPEN_DST_TEST_MODE = 2,
FIO_RUST_OPEN_DST_STDOUT = 3,
FIO_RUST_OPEN_DST_SAME_FILE = 4,
FIO_RUST_OPEN_DST_EXISTING = 5,
FIO_RUST_OPEN_DST_NULL_DEVICE_REGULAR = 6,
FIO_RUST_OPEN_DST_OPEN_FAILED = 7,
};
int FIO_rust_openDstFile(int testMode,
int allowExisting,
const char* srcFileName,
const char* dstFileName,
int mode,
int* isDstRegFile,
FILE** outFile);
int FIO_rust_setDictBufferMalloc(const char* fileName,
unsigned long long expectedFileSize,
size_t maxSize,
@@ -478,99 +495,88 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs,
const char* srcFileName, const char* dstFileName,
const int mode)
{
int isDstRegFile;
int isDstRegFile = 0;
int allowExisting = 0;
int sparseAdjusted = 0;
int status;
FILE* f = NULL;
if (prefs->testMode) return NULL; /* do not open file in test mode */
status = FIO_rust_openDstFile(prefs->testMode, allowExisting,
srcFileName, dstFileName, mode,
&isDstRegFile, &f);
if (status == FIO_RUST_OPEN_DST_TEST_MODE)
return NULL; /* do not open file in test mode */
assert(dstFileName != NULL);
if (!strcmp (dstFileName, stdoutmark)) {
DISPLAYLEVEL(4,"Using stdout for output \n");
SET_BINARY_MODE(stdout);
if (prefs->sparseFileSupport == 1) {
prefs->sparseFileSupport = 0;
DISPLAYLEVEL(4, "Sparse File Support is automatically disabled on stdout ; try --sparse \n");
for (;;) {
if (status == FIO_RUST_OPEN_DST_STDOUT) {
DISPLAYLEVEL(4,"Using stdout for output \n");
SET_BINARY_MODE(stdout);
if (prefs->sparseFileSupport == 1) {
prefs->sparseFileSupport = 0;
DISPLAYLEVEL(4, "Sparse File Support is automatically disabled on stdout ; try --sparse \n");
}
return stdout;
}
return stdout;
}
/* ensure dst is not the same as src */
if (srcFileName != NULL && UTIL_isSameFile(srcFileName, dstFileName)) {
DISPLAYLEVEL(1, "zstd: Refusing to open an output file which will overwrite the input file \n");
return NULL;
}
isDstRegFile = UTIL_isRegularFile(dstFileName); /* invoke once */
if (prefs->sparseFileSupport == 1) {
prefs->sparseFileSupport = ZSTD_SPARSE_DEFAULT;
if (!isDstRegFile) {
prefs->sparseFileSupport = 0;
DISPLAYLEVEL(4, "Sparse File Support is disabled when output is not a file \n");
if (status == FIO_RUST_OPEN_DST_SAME_FILE) {
DISPLAYLEVEL(1, "zstd: Refusing to open an output file which will overwrite the input file \n");
return NULL;
}
}
if (isDstRegFile) {
/* Check if destination file already exists */
#if !defined(_WIN32)
/* this test does not work on Windows :
* `NUL` and `nul` are detected as regular files */
if (!strcmp(dstFileName, nulmark)) {
/* Keep the C-owned preference mutation and its diagnostics here. Do
* this only for the first classification: after an existing file is
* removed, the second Rust call must not reclassify it as a missing
* destination and emit a different sparse-mode message. */
if (!sparseAdjusted) {
if (prefs->sparseFileSupport == 1) {
prefs->sparseFileSupport = ZSTD_SPARSE_DEFAULT;
if (!isDstRegFile) {
prefs->sparseFileSupport = 0;
DISPLAYLEVEL(4, "Sparse File Support is disabled when output is not a file \n");
}
}
sparseAdjusted = 1;
}
if (status == FIO_RUST_OPEN_DST_NULL_DEVICE_REGULAR) {
EXM_THROW(40, "%s is unexpectedly categorized as a regular file",
dstFileName);
}
#endif
if (!prefs->overwrite) {
if (g_display_prefs.displayLevel <= 1) {
/* No interaction possible */
DISPLAYLEVEL(1, "zstd: %s already exists; not overwritten \n",
dstFileName);
return NULL;
}
DISPLAY("zstd: %s already exists; ", dstFileName);
if (UTIL_requireUserConfirmation("overwrite (y/n) ? ", "Not overwritten \n", "yY", fCtx->hasStdinInput))
return NULL;
}
/* need to unlink */
FIO_removeFile(dstFileName);
}
{
#if defined(_WIN32)
/* Windows requires opening the file as a "binary" file to avoid
* mangling. This macro doesn't exist on unix. */
const int openflags = O_WRONLY|O_CREAT|O_TRUNC|O_BINARY;
const int fd = _open(dstFileName, openflags, mode);
FILE* f = NULL;
if (fd != -1) {
f = _fdopen(fd, "wb");
}
#else
const int openflags = O_WRONLY|O_CREAT|O_TRUNC;
const int fd = open(dstFileName, openflags, mode);
FILE* f = NULL;
if (fd != -1) {
f = fdopen(fd, "wb");
}
#endif
if (f == NULL) {
DISPLAYLEVEL(1, "zstd: %s: %s\n", dstFileName, strerror(errno));
} else {
/* An increased buffer size can provide a significant performance
* boost on some platforms. Note that providing a NULL buf with a
* size that's not 0 is not defined in ANSI C, but is defined in an
* extension. There are three possibilities here:
* 1. Libc supports the extended version and everything is good.
* 2. Libc ignores the size when buf is NULL, in which case
* everything will continue as if we didn't call `setvbuf()`.
* 3. We fail the call and execution continues but a warning
* message might be shown.
* In all cases due execution continues. For now, I believe that
* this is a more cost-effective solution than managing the buffers
* allocations ourselves (will require an API change).
*/
if (setvbuf(f, NULL, _IOFBF, 1 MB)) {
DISPLAYLEVEL(2, "Warning: setvbuf failed for %s\n", dstFileName);
if (status == FIO_RUST_OPEN_DST_EXISTING) {
if (!prefs->overwrite) {
if (g_display_prefs.displayLevel <= 1) {
/* No interaction possible */
DISPLAYLEVEL(1, "zstd: %s already exists; not overwritten \n",
dstFileName);
return NULL;
}
DISPLAY("zstd: %s already exists; ", dstFileName);
if (UTIL_requireUserConfirmation("overwrite (y/n) ? ", "Not overwritten \n", "yY", fCtx->hasStdinInput))
return NULL;
}
/* Keep the existing C wrapper so its stat/non-regular diagnostics
* remain unchanged. The next Rust call opens with O_TRUNC. */
FIO_removeFile(dstFileName);
allowExisting = 1;
f = NULL;
status = FIO_rust_openDstFile(prefs->testMode, allowExisting,
srcFileName, dstFileName, mode,
&isDstRegFile, &f);
continue;
}
if (status == FIO_RUST_OPEN_DST_OPEN_FAILED) {
DISPLAYLEVEL(1, "zstd: %s: %s\n", dstFileName, strerror(errno));
return NULL;
}
if (status == FIO_RUST_OPEN_DST_SETBUF_FAILED)
DISPLAYLEVEL(2, "Warning: setvbuf failed for %s\n", dstFileName);
assert(status == FIO_RUST_OPEN_DST_SUCCESS
|| status == FIO_RUST_OPEN_DST_SETBUF_FAILED);
return f;
}
}