feat(cli): move destination-open orchestration to Rust
FIO_openDstFile() still owned the complete destination policy loop in C even though the filesystem opener and status/action classifier were already Rust leaves. That left confirmation, sparse-mode adjustment, overwrite removal, and retry ordering duplicated beside private FILE* and CLI state. Project the C-owned callbacks for diagnostics, preference mutation, user confirmation, stdout setup, and existing-file removal. Rust now owns the retry/order state machine and calls the existing narrow opener for each attempt; C keeps private preferences and context, exact diagnostics, and FILE* ownership. The callback layout is asserted on both sides, and focused tests cover status ordering, confirmation/removal retry, setvbuf preservation, and invalid policy inputs. Test Plan: - `ulimit -v 41943040; CARGO_BUILD_JOBS=1 make -j1` -- passed, including the single-thread library, MT library, and CLI binary. - `ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets -- -D warnings` -- passed. - `ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo test --manifest-path rust/cli/Cargo.toml --all-targets` -- passed: 206 tests. - `git diff --cached --check` -- passed.
This commit is contained in:
+138
-103
@@ -358,8 +358,8 @@ int FIO_rust_multiFilesConcatWarning(int nbFilesTotal,
|
|||||||
|
|
||||||
|
|
||||||
/* These policy and filesystem leaves are implemented by the Rust CLI archive.
|
/* These policy and filesystem leaves are implemented by the Rust CLI archive.
|
||||||
* The declarations in fileio.h remain the C ABI shims while file-I/O
|
* The declarations in fileio.h remain the C ABI shims while C keeps private
|
||||||
* orchestration and diagnostics stay here. */
|
* resource/layout state and exact diagnostics around the Rust policy calls. */
|
||||||
/* FIO_highbit64() is a Rust-owned scalar policy leaf. */
|
/* FIO_highbit64() is a Rust-owned scalar policy leaf. */
|
||||||
unsigned FIO_highbit64(unsigned long long v);
|
unsigned FIO_highbit64(unsigned long long v);
|
||||||
unsigned long long FIO_getLargestFileSize(const char** inFileNames, unsigned nbFiles);
|
unsigned long long FIO_getLargestFileSize(const char** inFileNames, unsigned nbFiles);
|
||||||
@@ -439,6 +439,44 @@ enum {
|
|||||||
};
|
};
|
||||||
int FIO_rust_openDstFileAction(int status, int overwrite, int displayLevel,
|
int FIO_rust_openDstFileAction(int status, int overwrite, int displayLevel,
|
||||||
int confirmation, int sparseAdjusted);
|
int confirmation, int sparseAdjusted);
|
||||||
|
typedef void (*FIO_rust_open_dst_action_fn)(void* opaque, int action,
|
||||||
|
const char* dstFileName);
|
||||||
|
typedef void (*FIO_rust_open_dst_adjust_sparse_fn)(void* opaque,
|
||||||
|
int isDstRegFile);
|
||||||
|
typedef void* (*FIO_rust_open_dst_stdout_fn)(void* opaque);
|
||||||
|
typedef int (*FIO_rust_open_dst_confirm_fn)(void* opaque);
|
||||||
|
typedef void (*FIO_rust_open_dst_remove_fn)(void* opaque,
|
||||||
|
const char* dstFileName);
|
||||||
|
typedef struct {
|
||||||
|
void* opaque;
|
||||||
|
FIO_rust_open_dst_action_fn displayAction;
|
||||||
|
FIO_rust_open_dst_adjust_sparse_fn adjustSparse;
|
||||||
|
FIO_rust_open_dst_stdout_fn useStdout;
|
||||||
|
FIO_rust_open_dst_confirm_fn confirm;
|
||||||
|
FIO_rust_open_dst_remove_fn removeExisting;
|
||||||
|
} FIO_rust_openDstFileCallbacks_t;
|
||||||
|
typedef char FIO_rust_open_dst_callback_sizes[
|
||||||
|
(sizeof(FIO_rust_open_dst_action_fn) == sizeof(void*)
|
||||||
|
&& sizeof(FIO_rust_open_dst_adjust_sparse_fn) == sizeof(void*)
|
||||||
|
&& sizeof(FIO_rust_open_dst_stdout_fn) == sizeof(void*)
|
||||||
|
&& sizeof(FIO_rust_open_dst_confirm_fn) == sizeof(void*)
|
||||||
|
&& sizeof(FIO_rust_open_dst_remove_fn) == sizeof(void*)) ? 1 : -1];
|
||||||
|
typedef char FIO_rust_open_dst_callback_offsets[
|
||||||
|
(offsetof(FIO_rust_openDstFileCallbacks_t, opaque) == 0
|
||||||
|
&& offsetof(FIO_rust_openDstFileCallbacks_t, displayAction) == sizeof(void*)
|
||||||
|
&& offsetof(FIO_rust_openDstFileCallbacks_t, adjustSparse) == 2 * sizeof(void*)
|
||||||
|
&& offsetof(FIO_rust_openDstFileCallbacks_t, useStdout) == 3 * sizeof(void*)
|
||||||
|
&& offsetof(FIO_rust_openDstFileCallbacks_t, confirm) == 4 * sizeof(void*)
|
||||||
|
&& offsetof(FIO_rust_openDstFileCallbacks_t, removeExisting) == 5 * sizeof(void*)
|
||||||
|
&& sizeof(FIO_rust_openDstFileCallbacks_t) == 6 * sizeof(void*)) ? 1 : -1];
|
||||||
|
void* FIO_rust_openDstFileOrchestrate(
|
||||||
|
int testMode,
|
||||||
|
int overwrite,
|
||||||
|
int displayLevel,
|
||||||
|
const char* srcFileName,
|
||||||
|
const char* dstFileName,
|
||||||
|
int mode,
|
||||||
|
const FIO_rust_openDstFileCallbacks_t* callbacks);
|
||||||
int FIO_rust_openDstFile(int testMode,
|
int FIO_rust_openDstFile(int testMode,
|
||||||
int allowExisting,
|
int allowExisting,
|
||||||
const char* srcFileName,
|
const char* srcFileName,
|
||||||
@@ -780,6 +818,92 @@ static FILE* FIO_openSrcFile(const FIO_prefs_t* const prefs, const char* srcFile
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* C-owned state for the Rust destination-open policy callbacks. */
|
||||||
|
typedef struct {
|
||||||
|
FIO_ctx_t* fCtx;
|
||||||
|
FIO_prefs_t* prefs;
|
||||||
|
const char* dstFileName;
|
||||||
|
} FIO_openDstFileCallbackContext_t;
|
||||||
|
|
||||||
|
static void
|
||||||
|
FIO_openDstAdjustSparse(void* opaque, int isDstRegFile)
|
||||||
|
{
|
||||||
|
FIO_openDstFileCallbackContext_t* const context =
|
||||||
|
(FIO_openDstFileCallbackContext_t*)opaque;
|
||||||
|
if (context->prefs->sparseFileSupport == 1) {
|
||||||
|
context->prefs->sparseFileSupport = ZSTD_SPARSE_DEFAULT;
|
||||||
|
if (!isDstRegFile) {
|
||||||
|
context->prefs->sparseFileSupport = 0;
|
||||||
|
DISPLAYLEVEL(4, "Sparse File Support is disabled when output is not a file \n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void*
|
||||||
|
FIO_openDstUseStdout(void* opaque)
|
||||||
|
{
|
||||||
|
FIO_openDstFileCallbackContext_t* const context =
|
||||||
|
(FIO_openDstFileCallbackContext_t*)opaque;
|
||||||
|
DISPLAYLEVEL(4,"Using stdout for output \n");
|
||||||
|
SET_BINARY_MODE(stdout);
|
||||||
|
if (context->prefs->sparseFileSupport == 1) {
|
||||||
|
context->prefs->sparseFileSupport = 0;
|
||||||
|
DISPLAYLEVEL(4, "Sparse File Support is automatically disabled on stdout ; try --sparse \n");
|
||||||
|
}
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
FIO_openDstDisplayAction(void* opaque, int action, const char* dstFileName)
|
||||||
|
{
|
||||||
|
(void)opaque;
|
||||||
|
switch (action) {
|
||||||
|
case FIO_RUST_OPEN_DST_ACTION_SAME_FILE:
|
||||||
|
DISPLAYLEVEL(1, "zstd: Refusing to open an output file which will overwrite the input file \n");
|
||||||
|
break;
|
||||||
|
case FIO_RUST_OPEN_DST_ACTION_NULL_DEVICE_REGULAR:
|
||||||
|
EXM_THROW(40, "%s is unexpectedly categorized as a regular file",
|
||||||
|
dstFileName);
|
||||||
|
return;
|
||||||
|
case FIO_RUST_OPEN_DST_ACTION_EXISTING_QUIET_ABORT:
|
||||||
|
DISPLAYLEVEL(1, "zstd: %s already exists; not overwritten \n",
|
||||||
|
dstFileName);
|
||||||
|
break;
|
||||||
|
case FIO_RUST_OPEN_DST_ACTION_OPEN_FAILED:
|
||||||
|
DISPLAYLEVEL(1, "zstd: %s: %s\n", dstFileName, strerror(errno));
|
||||||
|
break;
|
||||||
|
case FIO_RUST_OPEN_DST_ACTION_SETBUF_FAILED:
|
||||||
|
DISPLAYLEVEL(2, "Warning: setvbuf failed for %s\n", dstFileName);
|
||||||
|
break;
|
||||||
|
case FIO_RUST_OPEN_DST_ACTION_INVALID:
|
||||||
|
default:
|
||||||
|
assert(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
FIO_openDstConfirm(void* opaque)
|
||||||
|
{
|
||||||
|
FIO_openDstFileCallbackContext_t* const context =
|
||||||
|
(FIO_openDstFileCallbackContext_t*)opaque;
|
||||||
|
DISPLAY("zstd: %s already exists; ", context->dstFileName);
|
||||||
|
return UTIL_requireUserConfirmation(
|
||||||
|
"overwrite (y/n) ? ", "Not overwritten \n", "yY",
|
||||||
|
context->fCtx->hasStdinInput)
|
||||||
|
? FIO_RUST_OPEN_DST_CONFIRMATION_ABORT
|
||||||
|
: FIO_RUST_OPEN_DST_CONFIRMATION_ACCEPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
FIO_openDstRemoveExisting(void* opaque, const char* dstFileName)
|
||||||
|
{
|
||||||
|
(void)opaque;
|
||||||
|
/* Keep the existing C wrapper so its stat/non-regular diagnostics remain
|
||||||
|
* unchanged. Rust calls this only after the overwrite policy allows it. */
|
||||||
|
(void)FIO_removeFile(dstFileName);
|
||||||
|
}
|
||||||
|
|
||||||
/** FIO_openDstFile() :
|
/** FIO_openDstFile() :
|
||||||
* condition : `dstFileName` must be non-NULL.
|
* condition : `dstFileName` must be non-NULL.
|
||||||
* @result : FILE* to `dstFileName`, or NULL if it fails */
|
* @result : FILE* to `dstFileName`, or NULL if it fails */
|
||||||
@@ -788,107 +912,18 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs,
|
|||||||
const char* srcFileName, const char* dstFileName,
|
const char* srcFileName, const char* dstFileName,
|
||||||
const int mode)
|
const int mode)
|
||||||
{
|
{
|
||||||
int isDstRegFile = 0;
|
FIO_openDstFileCallbackContext_t context = { fCtx, prefs, dstFileName };
|
||||||
int allowExisting = 0;
|
FIO_rust_openDstFileCallbacks_t callbacks = {
|
||||||
int sparseAdjusted = 0;
|
&context,
|
||||||
int confirmation = FIO_RUST_OPEN_DST_CONFIRMATION_UNASKED;
|
FIO_openDstDisplayAction,
|
||||||
int status;
|
FIO_openDstAdjustSparse,
|
||||||
FILE* f = NULL;
|
FIO_openDstUseStdout,
|
||||||
|
FIO_openDstConfirm,
|
||||||
status = FIO_rust_openDstFile(prefs->testMode, allowExisting,
|
FIO_openDstRemoveExisting
|
||||||
srcFileName, dstFileName, mode,
|
};
|
||||||
&isDstRegFile, &f);
|
return (FILE*)FIO_rust_openDstFileOrchestrate(
|
||||||
for (;;) {
|
prefs->testMode, prefs->overwrite, g_display_prefs.displayLevel,
|
||||||
int const action = FIO_rust_openDstFileAction(
|
srcFileName, dstFileName, mode, &callbacks);
|
||||||
status, prefs->overwrite, g_display_prefs.displayLevel,
|
|
||||||
confirmation, sparseAdjusted);
|
|
||||||
|
|
||||||
if (action == FIO_RUST_OPEN_DST_ACTION_TEST_MODE)
|
|
||||||
return NULL; /* do not open file in test mode */
|
|
||||||
|
|
||||||
assert(dstFileName != NULL);
|
|
||||||
switch (action) {
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_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;
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_SAME_FILE:
|
|
||||||
DISPLAYLEVEL(1, "zstd: Refusing to open an output file which will overwrite the input file \n");
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_ADJUST_SPARSE:
|
|
||||||
/* Rust orders this once-only policy action before all ordinary
|
|
||||||
* destination outcomes. Keep the preference mutation and its
|
|
||||||
* diagnostic in C, and let the next loop iteration classify the
|
|
||||||
* same status again. */
|
|
||||||
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;
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_NULL_DEVICE_REGULAR:
|
|
||||||
EXM_THROW(40, "%s is unexpectedly categorized as a regular file",
|
|
||||||
dstFileName);
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_EXISTING_QUIET_ABORT:
|
|
||||||
/* No interaction possible. */
|
|
||||||
DISPLAYLEVEL(1, "zstd: %s already exists; not overwritten \n",
|
|
||||||
dstFileName);
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_EXISTING_PROMPT:
|
|
||||||
DISPLAY("zstd: %s already exists; ", dstFileName);
|
|
||||||
confirmation = UTIL_requireUserConfirmation(
|
|
||||||
"overwrite (y/n) ? ", "Not overwritten \n", "yY",
|
|
||||||
fCtx->hasStdinInput)
|
|
||||||
? FIO_RUST_OPEN_DST_CONFIRMATION_ABORT
|
|
||||||
: FIO_RUST_OPEN_DST_CONFIRMATION_ACCEPT;
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_EXISTING_ABORT:
|
|
||||||
/* UTIL_requireUserConfirmation() already emitted the exact
|
|
||||||
* existing diagnostic before returning the abort result. */
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_EXISTING_REMOVE:
|
|
||||||
/* 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;
|
|
||||||
confirmation = FIO_RUST_OPEN_DST_CONFIRMATION_UNASKED;
|
|
||||||
status = FIO_rust_openDstFile(prefs->testMode, allowExisting,
|
|
||||||
srcFileName, dstFileName, mode,
|
|
||||||
&isDstRegFile, &f);
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_OPEN_FAILED:
|
|
||||||
DISPLAYLEVEL(1, "zstd: %s: %s\n", dstFileName, strerror(errno));
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_SETBUF_FAILED:
|
|
||||||
DISPLAYLEVEL(2, "Warning: setvbuf failed for %s\n", dstFileName);
|
|
||||||
return f;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_SUCCESS:
|
|
||||||
return f;
|
|
||||||
|
|
||||||
case FIO_RUST_OPEN_DST_ACTION_INVALID:
|
|
||||||
default:
|
|
||||||
assert(0);
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+330
-6
@@ -1,9 +1,11 @@
|
|||||||
|
#![allow(non_camel_case_types)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
#![allow(clippy::missing_safety_doc)]
|
#![allow(clippy::missing_safety_doc)]
|
||||||
|
|
||||||
//! Rust-owned filesystem leaves for the command-line backend.
|
//! Rust-owned filesystem leaves for the command-line backend.
|
||||||
//!
|
//!
|
||||||
//! The surrounding CLI still owns diagnostics and stream orchestration in C.
|
//! The surrounding CLI still owns diagnostics and remaining stream
|
||||||
|
//! orchestration in C.
|
||||||
//! This module implements the filesystem operations behind
|
//! This module implements the filesystem operations behind
|
||||||
//! `FIO_openDstFile()`, `FIO_openSrcFile()`, `FIO_removeFile()`, and dictionary
|
//! `FIO_openDstFile()`, `FIO_openSrcFile()`, `FIO_removeFile()`, and dictionary
|
||||||
//! loading, returning small status codes so the C wrappers can retain their
|
//! loading, returning small status codes so the C wrappers can retain their
|
||||||
@@ -16,6 +18,7 @@
|
|||||||
use std::ffi::{c_char, c_void, CStr};
|
use std::ffi::{c_char, c_void, CStr};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
use std::os::raw::c_int;
|
use std::os::raw::c_int;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
@@ -160,6 +163,41 @@ impl DestinationAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FIO_openDstActionFn = unsafe extern "C" fn(*mut c_void, c_int, *const c_char);
|
||||||
|
type FIO_openDstAdjustSparseFn = unsafe extern "C" fn(*mut c_void, c_int);
|
||||||
|
type FIO_openDstStdoutFn = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
|
||||||
|
type FIO_openDstConfirmFn = unsafe extern "C" fn(*mut c_void) -> c_int;
|
||||||
|
type FIO_openDstRemoveFn = unsafe extern "C" fn(*mut c_void, *const c_char);
|
||||||
|
|
||||||
|
/// Callbacks for destination-open side effects that depend on C's private CLI
|
||||||
|
/// state. Rust owns the retry/order policy; C retains preferences, context,
|
||||||
|
/// exact diagnostics, binary-mode setup, and file removal.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct FIO_rust_openDstFileCallbacks_t {
|
||||||
|
opaque: *mut c_void,
|
||||||
|
displayAction: Option<FIO_openDstActionFn>,
|
||||||
|
adjustSparse: Option<FIO_openDstAdjustSparseFn>,
|
||||||
|
useStdout: Option<FIO_openDstStdoutFn>,
|
||||||
|
confirm: Option<FIO_openDstConfirmFn>,
|
||||||
|
removeExisting: Option<FIO_openDstRemoveFn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
let word = size_of::<usize>();
|
||||||
|
assert!(offset_of!(FIO_rust_openDstFileCallbacks_t, opaque) == 0);
|
||||||
|
assert!(offset_of!(FIO_rust_openDstFileCallbacks_t, displayAction) == word);
|
||||||
|
assert!(offset_of!(FIO_rust_openDstFileCallbacks_t, adjustSparse) == 2 * word);
|
||||||
|
assert!(offset_of!(FIO_rust_openDstFileCallbacks_t, useStdout) == 3 * word);
|
||||||
|
assert!(offset_of!(FIO_rust_openDstFileCallbacks_t, confirm) == 4 * word);
|
||||||
|
assert!(offset_of!(FIO_rust_openDstFileCallbacks_t, removeExisting) == 5 * word);
|
||||||
|
assert!(size_of::<FIO_rust_openDstFileCallbacks_t>() == 6 * word);
|
||||||
|
assert!(size_of::<Option<FIO_openDstActionFn>>() == word);
|
||||||
|
assert!(size_of::<Option<FIO_openDstAdjustSparseFn>>() == word);
|
||||||
|
assert!(size_of::<Option<FIO_openDstStdoutFn>>() == word);
|
||||||
|
assert!(size_of::<Option<FIO_openDstConfirmFn>>() == word);
|
||||||
|
assert!(size_of::<Option<FIO_openDstRemoveFn>>() == word);
|
||||||
|
};
|
||||||
|
|
||||||
fn is_known_destination_status(status: c_int) -> bool {
|
fn is_known_destination_status(status: c_int) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
status,
|
status,
|
||||||
@@ -178,11 +216,10 @@ fn is_known_destination_status(status: c_int) -> bool {
|
|||||||
/// CLI state.
|
/// CLI state.
|
||||||
///
|
///
|
||||||
/// Sparse-mode adjustment is returned as its own first action because C owns
|
/// Sparse-mode adjustment is returned as its own first action because C owns
|
||||||
/// the preference field and its diagnostic. C calls back with
|
/// the preference field and its diagnostic. Rust invokes the C callback once
|
||||||
/// `sparse_adjusted = 1` after applying that mutation, then this same policy
|
/// and then reclassifies the same status with `sparse_adjusted = 1`. Likewise,
|
||||||
/// selects the status-specific action. Likewise, C owns the confirmation
|
/// C owns the confirmation callback and supplies its result on the next call.
|
||||||
/// callback and supplies its result on the next call. This keeps the retry
|
/// This keeps FILE* ownership, metadata, and exact diagnostics in C while the
|
||||||
/// mechanics, FILE* ownership, metadata, and exact diagnostics in C while the
|
|
||||||
/// ordering between those operations is centralized here.
|
/// ordering between those operations is centralized here.
|
||||||
fn destination_action(
|
fn destination_action(
|
||||||
status: c_int,
|
status: c_int,
|
||||||
@@ -249,6 +286,175 @@ pub extern "C" fn FIO_rust_openDstFileAction(
|
|||||||
.code()
|
.code()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
struct DestinationOpenResult {
|
||||||
|
status: c_int,
|
||||||
|
is_regular: c_int,
|
||||||
|
file: *mut c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait DestinationOpenCallbacks {
|
||||||
|
fn adjust_sparse(&mut self, is_regular: c_int);
|
||||||
|
fn use_stdout(&mut self) -> *mut c_void;
|
||||||
|
fn display_action(&mut self, action: DestinationAction, dst_file_name: *const c_char);
|
||||||
|
fn confirm(&mut self) -> c_int;
|
||||||
|
fn remove_existing(&mut self, dst_file_name: *const c_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct COpenDstCallbacks {
|
||||||
|
callbacks: *const FIO_rust_openDstFileCallbacks_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DestinationOpenCallbacks for COpenDstCallbacks {
|
||||||
|
fn adjust_sparse(&mut self, is_regular: c_int) {
|
||||||
|
let callbacks = unsafe { &*self.callbacks };
|
||||||
|
let callback = callbacks
|
||||||
|
.adjustSparse
|
||||||
|
.expect("destination sparse callback is required");
|
||||||
|
unsafe { callback(callbacks.opaque, is_regular) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_stdout(&mut self) -> *mut c_void {
|
||||||
|
let callbacks = unsafe { &*self.callbacks };
|
||||||
|
let callback = callbacks
|
||||||
|
.useStdout
|
||||||
|
.expect("destination stdout callback is required");
|
||||||
|
unsafe { callback(callbacks.opaque) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_action(&mut self, action: DestinationAction, dst_file_name: *const c_char) {
|
||||||
|
let callbacks = unsafe { &*self.callbacks };
|
||||||
|
let callback = callbacks
|
||||||
|
.displayAction
|
||||||
|
.expect("destination action callback is required");
|
||||||
|
unsafe { callback(callbacks.opaque, action.code(), dst_file_name) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self) -> c_int {
|
||||||
|
let callbacks = unsafe { &*self.callbacks };
|
||||||
|
let callback = callbacks
|
||||||
|
.confirm
|
||||||
|
.expect("destination confirmation callback is required");
|
||||||
|
unsafe { callback(callbacks.opaque) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_existing(&mut self, dst_file_name: *const c_char) {
|
||||||
|
let callbacks = unsafe { &*self.callbacks };
|
||||||
|
let callback = callbacks
|
||||||
|
.removeExisting
|
||||||
|
.expect("destination remove callback is required");
|
||||||
|
unsafe { callback(callbacks.opaque, dst_file_name) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the destination-open retry policy around C-owned side effects.
|
||||||
|
///
|
||||||
|
/// The opener returns the status and private `FILE*` result for one attempt.
|
||||||
|
/// Rust orders the sparse adjustment, confirmation, remove-and-retry, and
|
||||||
|
/// terminal actions; callbacks keep C's preference/context layouts and exact
|
||||||
|
/// diagnostics out of this module.
|
||||||
|
fn orchestrate_destination_open<F, C>(
|
||||||
|
overwrite: c_int,
|
||||||
|
display_level: c_int,
|
||||||
|
dst_file_name: *const c_char,
|
||||||
|
callbacks: &mut C,
|
||||||
|
mut open: F,
|
||||||
|
) -> *mut c_void
|
||||||
|
where
|
||||||
|
F: FnMut(c_int) -> DestinationOpenResult,
|
||||||
|
C: DestinationOpenCallbacks,
|
||||||
|
{
|
||||||
|
let mut sparse_adjusted = 0;
|
||||||
|
let mut confirmation = FIO_OPEN_DST_CONFIRMATION_UNASKED;
|
||||||
|
let mut result = open(0);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let action = destination_action(
|
||||||
|
result.status,
|
||||||
|
overwrite,
|
||||||
|
display_level,
|
||||||
|
confirmation,
|
||||||
|
sparse_adjusted,
|
||||||
|
);
|
||||||
|
|
||||||
|
match action {
|
||||||
|
DestinationAction::TestMode => return ptr::null_mut(),
|
||||||
|
DestinationAction::Stdout => return callbacks.use_stdout(),
|
||||||
|
DestinationAction::SameFile | DestinationAction::ExistingQuietAbort => {
|
||||||
|
callbacks.display_action(action, dst_file_name);
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
DestinationAction::AdjustSparse => {
|
||||||
|
callbacks.adjust_sparse(result.is_regular);
|
||||||
|
sparse_adjusted = 1;
|
||||||
|
}
|
||||||
|
DestinationAction::NullDeviceRegular
|
||||||
|
| DestinationAction::OpenFailed
|
||||||
|
| DestinationAction::Invalid => {
|
||||||
|
callbacks.display_action(action, dst_file_name);
|
||||||
|
return result.file;
|
||||||
|
}
|
||||||
|
DestinationAction::ExistingPrompt => {
|
||||||
|
confirmation = callbacks.confirm();
|
||||||
|
}
|
||||||
|
DestinationAction::ExistingAbort => return ptr::null_mut(),
|
||||||
|
DestinationAction::ExistingRemove => {
|
||||||
|
callbacks.remove_existing(dst_file_name);
|
||||||
|
confirmation = FIO_OPEN_DST_CONFIRMATION_UNASKED;
|
||||||
|
result = open(1);
|
||||||
|
}
|
||||||
|
DestinationAction::SetbufFailed => {
|
||||||
|
callbacks.display_action(action, dst_file_name);
|
||||||
|
return result.file;
|
||||||
|
}
|
||||||
|
DestinationAction::Success => return result.file,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the destination-open retry/order loop behind the Rust backend while
|
||||||
|
/// preserving C-owned preferences, context, diagnostics, and `FILE*` handles.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn FIO_rust_openDstFileOrchestrate(
|
||||||
|
test_mode: c_int,
|
||||||
|
overwrite: c_int,
|
||||||
|
display_level: c_int,
|
||||||
|
src_file_name: *const c_char,
|
||||||
|
dst_file_name: *const c_char,
|
||||||
|
mode: c_int,
|
||||||
|
callbacks: *const FIO_rust_openDstFileCallbacks_t,
|
||||||
|
) -> *mut c_void {
|
||||||
|
assert!(!callbacks.is_null());
|
||||||
|
|
||||||
|
let mut callbacks = COpenDstCallbacks { callbacks };
|
||||||
|
orchestrate_destination_open(
|
||||||
|
overwrite,
|
||||||
|
display_level,
|
||||||
|
dst_file_name,
|
||||||
|
&mut callbacks,
|
||||||
|
|allow_existing| {
|
||||||
|
let mut is_regular = 0;
|
||||||
|
let mut file = ptr::null_mut();
|
||||||
|
let status = unsafe {
|
||||||
|
FIO_rust_openDstFile(
|
||||||
|
test_mode,
|
||||||
|
allow_existing,
|
||||||
|
src_file_name,
|
||||||
|
dst_file_name,
|
||||||
|
mode,
|
||||||
|
&mut is_regular,
|
||||||
|
&mut file,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
DestinationOpenResult {
|
||||||
|
status,
|
||||||
|
is_regular,
|
||||||
|
file,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
unsafe extern "C" {
|
unsafe extern "C" {
|
||||||
fn UTIL_stat(file_name: *const c_char, stat_buf: *mut libc::stat) -> c_int;
|
fn UTIL_stat(file_name: *const c_char, stat_buf: *mut libc::stat) -> c_int;
|
||||||
fn UTIL_isRegularFileStat(stat_buf: *const libc::stat) -> c_int;
|
fn UTIL_isRegularFileStat(stat_buf: *const libc::stat) -> c_int;
|
||||||
@@ -1215,6 +1421,124 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum DestinationTestEvent {
|
||||||
|
AdjustSparse(c_int),
|
||||||
|
Confirm,
|
||||||
|
Remove,
|
||||||
|
Display(DestinationAction),
|
||||||
|
Stdout,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DestinationTestCallbacks {
|
||||||
|
events: Vec<DestinationTestEvent>,
|
||||||
|
confirmation: c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DestinationOpenCallbacks for DestinationTestCallbacks {
|
||||||
|
fn adjust_sparse(&mut self, is_regular: c_int) {
|
||||||
|
self.events
|
||||||
|
.push(DestinationTestEvent::AdjustSparse(is_regular));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_stdout(&mut self) -> *mut c_void {
|
||||||
|
self.events.push(DestinationTestEvent::Stdout);
|
||||||
|
ptr::dangling_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_action(&mut self, action: DestinationAction, _dst_file_name: *const c_char) {
|
||||||
|
self.events.push(DestinationTestEvent::Display(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self) -> c_int {
|
||||||
|
self.events.push(DestinationTestEvent::Confirm);
|
||||||
|
self.confirmation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_existing(&mut self, _dst_file_name: *const c_char) {
|
||||||
|
self.events.push(DestinationTestEvent::Remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn destination_open_orchestration_retries_after_confirmation_and_removal() {
|
||||||
|
let expected_file = ptr::dangling_mut::<c_void>();
|
||||||
|
let mut callbacks = DestinationTestCallbacks {
|
||||||
|
events: Vec::new(),
|
||||||
|
confirmation: FIO_OPEN_DST_CONFIRMATION_ACCEPT,
|
||||||
|
};
|
||||||
|
let mut open_calls = Vec::new();
|
||||||
|
let mut results = vec![
|
||||||
|
DestinationOpenResult {
|
||||||
|
status: FIO_OPEN_DST_EXISTING,
|
||||||
|
is_regular: 1,
|
||||||
|
file: ptr::null_mut(),
|
||||||
|
},
|
||||||
|
DestinationOpenResult {
|
||||||
|
status: FIO_OPEN_DST_SUCCESS,
|
||||||
|
is_regular: 1,
|
||||||
|
file: expected_file,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let file = orchestrate_destination_open(
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
ptr::null(),
|
||||||
|
&mut callbacks,
|
||||||
|
|allow_existing| {
|
||||||
|
open_calls.push(allow_existing);
|
||||||
|
results.remove(0)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(file, expected_file);
|
||||||
|
assert_eq!(open_calls, vec![0, 1]);
|
||||||
|
assert_eq!(
|
||||||
|
callbacks.events,
|
||||||
|
vec![
|
||||||
|
DestinationTestEvent::AdjustSparse(1),
|
||||||
|
DestinationTestEvent::Confirm,
|
||||||
|
DestinationTestEvent::Remove,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn destination_open_orchestration_preserves_setbuf_file_after_policy() {
|
||||||
|
let expected_file = ptr::dangling_mut::<c_void>();
|
||||||
|
let mut callbacks = DestinationTestCallbacks {
|
||||||
|
events: Vec::new(),
|
||||||
|
confirmation: FIO_OPEN_DST_CONFIRMATION_ABORT,
|
||||||
|
};
|
||||||
|
let mut open_calls = Vec::new();
|
||||||
|
|
||||||
|
let file = orchestrate_destination_open(
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
ptr::null(),
|
||||||
|
&mut callbacks,
|
||||||
|
|allow_existing| {
|
||||||
|
open_calls.push(allow_existing);
|
||||||
|
DestinationOpenResult {
|
||||||
|
status: FIO_OPEN_DST_SETBUF_FAILED,
|
||||||
|
is_regular: 0,
|
||||||
|
file: expected_file,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(file, expected_file);
|
||||||
|
assert_eq!(open_calls, vec![0]);
|
||||||
|
assert_eq!(
|
||||||
|
callbacks.events,
|
||||||
|
vec![
|
||||||
|
DestinationTestEvent::AdjustSparse(0),
|
||||||
|
DestinationTestEvent::Display(DestinationAction::SetbufFailed),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dictionary_loader_diagnostics_preserve_each_backend_status_class() {
|
fn dictionary_loader_diagnostics_preserve_each_backend_status_class() {
|
||||||
let cases = [
|
let cases = [
|
||||||
|
|||||||
Reference in New Issue
Block a user