diff --git a/programs/fileio.c b/programs/fileio.c index 1927fe2b1..c21070ee8 100644 --- a/programs/fileio.c +++ b/programs/fileio.c @@ -358,8 +358,8 @@ int FIO_rust_multiFilesConcatWarning(int nbFilesTotal, /* 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 - * orchestration and diagnostics stay here. */ + * The declarations in fileio.h remain the C ABI shims while C keeps private + * resource/layout state and exact diagnostics around the Rust policy calls. */ /* FIO_highbit64() is a Rust-owned scalar policy leaf. */ unsigned FIO_highbit64(unsigned long long v); 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 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 allowExisting, 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() : * condition : `dstFileName` must be non-NULL. * @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 int mode) { - int isDstRegFile = 0; - int allowExisting = 0; - int sparseAdjusted = 0; - int confirmation = FIO_RUST_OPEN_DST_CONFIRMATION_UNASKED; - int status; - FILE* f = NULL; - - status = FIO_rust_openDstFile(prefs->testMode, allowExisting, - srcFileName, dstFileName, mode, - &isDstRegFile, &f); - for (;;) { - int const action = FIO_rust_openDstFileAction( - 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; - } - } + FIO_openDstFileCallbackContext_t context = { fCtx, prefs, dstFileName }; + FIO_rust_openDstFileCallbacks_t callbacks = { + &context, + FIO_openDstDisplayAction, + FIO_openDstAdjustSparse, + FIO_openDstUseStdout, + FIO_openDstConfirm, + FIO_openDstRemoveExisting + }; + return (FILE*)FIO_rust_openDstFileOrchestrate( + prefs->testMode, prefs->overwrite, g_display_prefs.displayLevel, + srcFileName, dstFileName, mode, &callbacks); } diff --git a/rust/src/fileio_backend.rs b/rust/src/fileio_backend.rs index eb69e7881..37f38c92d 100644 --- a/rust/src/fileio_backend.rs +++ b/rust/src/fileio_backend.rs @@ -1,9 +1,11 @@ +#![allow(non_camel_case_types)] #![allow(non_snake_case)] #![allow(clippy::missing_safety_doc)] //! 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 //! `FIO_openDstFile()`, `FIO_openSrcFile()`, `FIO_removeFile()`, and dictionary //! 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::fs::File; use std::io::Read; +use std::mem::{offset_of, size_of}; use std::os::raw::c_int; use std::path::{Path, PathBuf}; 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, + adjustSparse: Option, + useStdout: Option, + confirm: Option, + removeExisting: Option, +} + +const _: () = { + let word = size_of::(); + 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::() == 6 * word); + assert!(size_of::>() == word); + assert!(size_of::>() == word); + assert!(size_of::>() == word); + assert!(size_of::>() == word); + assert!(size_of::>() == word); +}; + fn is_known_destination_status(status: c_int) -> bool { matches!( status, @@ -178,11 +216,10 @@ fn is_known_destination_status(status: c_int) -> bool { /// CLI state. /// /// Sparse-mode adjustment is returned as its own first action because C owns -/// the preference field and its diagnostic. C calls back with -/// `sparse_adjusted = 1` after applying that mutation, then this same policy -/// selects the status-specific action. Likewise, C owns the confirmation -/// callback and supplies its result on the next call. This keeps the retry -/// mechanics, FILE* ownership, metadata, and exact diagnostics in C while the +/// the preference field and its diagnostic. Rust invokes the C callback once +/// and then reclassifies the same status with `sparse_adjusted = 1`. Likewise, +/// C owns the confirmation callback and supplies its result on the next call. +/// This keeps FILE* ownership, metadata, and exact diagnostics in C while the /// ordering between those operations is centralized here. fn destination_action( status: c_int, @@ -249,6 +286,175 @@ pub extern "C" fn FIO_rust_openDstFileAction( .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( + 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" { 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; @@ -1215,6 +1421,124 @@ mod tests { ); } + #[derive(Debug, PartialEq, Eq)] + enum DestinationTestEvent { + AdjustSparse(c_int), + Confirm, + Remove, + Display(DestinationAction), + Stdout, + } + + struct DestinationTestCallbacks { + events: Vec, + 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::(); + 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::(); + 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] fn dictionary_loader_diagnostics_preserve_each_backend_status_class() { let cases = [