diff --git a/programs/fileio.c b/programs/fileio.c index d5991c926..9d3880061 100644 --- a/programs/fileio.c +++ b/programs/fileio.c @@ -292,22 +292,62 @@ typedef char FIO_rust_compression_params_size[ int FIO_shouldDisplayFileSummary(const FIO_ctx_t* fCtx); int FIO_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); +typedef void (*FIO_rust_multi_files_concat_fatal_fn)(void* opaque); +typedef void (*FIO_rust_multi_files_concat_warning_fn)(void* opaque, + int hasStdoutOutput, + const char* outFileName); +typedef void (*FIO_rust_multi_files_concat_disable_remove_fn)(void* opaque); +typedef void (*FIO_rust_multi_files_concat_quiet_abort_fn)(void* opaque); +typedef int (*FIO_rust_multi_files_concat_confirmation_fn)(void* opaque, + int hasStdinInput); +typedef struct { + void* opaque; + FIO_rust_multi_files_concat_fatal_fn fatalStdoutRemove; + FIO_rust_multi_files_concat_fatal_fn fatalTestRemove; + FIO_rust_multi_files_concat_warning_fn displayWarning; + FIO_rust_multi_files_concat_disable_remove_fn disableRemove; + FIO_rust_multi_files_concat_quiet_abort_fn displayQuietAbort; + FIO_rust_multi_files_concat_confirmation_fn requireConfirmation; +} FIO_rust_multi_files_concat_callbacks_t; +typedef char FIO_rust_multi_files_concat_callback_sizes[ + (sizeof(FIO_rust_multi_files_concat_fatal_fn) == sizeof(void*) + && sizeof(FIO_rust_multi_files_concat_warning_fn) == sizeof(void*) + && sizeof(FIO_rust_multi_files_concat_disable_remove_fn) == sizeof(void*) + && sizeof(FIO_rust_multi_files_concat_quiet_abort_fn) == sizeof(void*) + && sizeof(FIO_rust_multi_files_concat_confirmation_fn) == sizeof(void*)) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_opaque_offset[ + (offsetof(FIO_rust_multi_files_concat_callbacks_t, opaque) == 0) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_fatal_stdout_offset[ + (offsetof(FIO_rust_multi_files_concat_callbacks_t, fatalStdoutRemove) + == sizeof(void*)) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_fatal_test_offset[ + (offsetof(FIO_rust_multi_files_concat_callbacks_t, fatalTestRemove) + == 2 * sizeof(void*)) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_warning_offset[ + (offsetof(FIO_rust_multi_files_concat_callbacks_t, displayWarning) + == 3 * sizeof(void*)) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_disable_remove_offset[ + (offsetof(FIO_rust_multi_files_concat_callbacks_t, disableRemove) + == 4 * sizeof(void*)) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_quiet_abort_offset[ + (offsetof(FIO_rust_multi_files_concat_callbacks_t, displayQuietAbort) + == 5 * sizeof(void*)) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_confirmation_offset[ + (offsetof(FIO_rust_multi_files_concat_callbacks_t, requireConfirmation) + == 6 * sizeof(void*)) ? 1 : -1]; +typedef char FIO_rust_multi_files_concat_callbacks_size[ + (sizeof(FIO_rust_multi_files_concat_callbacks_t) == 7 * sizeof(void*)) ? 1 : -1]; +int FIO_rust_multiFilesConcatWarning(int nbFilesTotal, + int hasStdoutOutput, + int testMode, + int hasOutputFile, + int removeSrcFile, + int overwrite, + int displayLevel, + int displayLevelCutoff, + int hasStdinInput, + const char* outFileName, + const FIO_rust_multi_files_concat_callbacks_t* callbacks); /*-************************************* * Parameters: Initialization @@ -1087,7 +1127,8 @@ static void FIO_adjustMemLimitForPatchFromMode(FIO_prefs_t* const prefs, } /* FIO_multiFilesConcatWarning() : - * This function handles logic when processing multiple files with -o or -c, displaying the appropriate warnings/prompts. + * This function supplies the C-owned diagnostics and prompt callbacks for the + * Rust policy/order wrapper used when processing multiple files with -o or -c. * Returns 1 if the console should abort, 0 if console should proceed. * * If output is stdout or test mode is active, check that `--rm` disabled. @@ -1101,60 +1142,88 @@ static void FIO_adjustMemLimitForPatchFromMode(FIO_prefs_t* const prefs, * If -f is specified or if output is stdout, just proceed. * If output is set with -o, prompt for confirmation. */ -static int FIO_multiFilesConcatWarning(const FIO_ctx_t* fCtx, FIO_prefs_t* prefs, const char* outFileName, int displayLevelCutoff) +typedef struct { + const FIO_ctx_t* fCtx; + FIO_prefs_t* prefs; +} FIO_multiFilesConcatCallbackContext_t; + +static void +FIO_multiFilesConcatFatalStdoutRemove(void* opaque) { - int action = FIO_rust_multiFilesConcatAction( - fCtx->nbFilesTotal, fCtx->hasStdoutOutput, prefs->testMode, - outFileName != NULL, prefs->removeSrcFile, prefs->overwrite, - g_display_prefs.displayLevel, displayLevelCutoff); + (void)opaque; + /* 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_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."); +static void +FIO_multiFilesConcatFatalTestRemove(void* opaque) +{ + (void)opaque; + /* 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 || prefs->testMode) return 0; - - if (fCtx->hasStdoutOutput) { +static void +FIO_multiFilesConcatDisplayWarning(void* opaque, int hasStdoutOutput, + const char* outFileName) +{ + (void)opaque; + if (hasStdoutOutput) { DISPLAYLEVEL(2, "zstd: WARNING: all input files will be processed and concatenated into stdout. \n"); } else { DISPLAYLEVEL(2, "zstd: WARNING: all input files will be processed and concatenated into a single output file: %s \n", outFileName); } DISPLAYLEVEL(2, "The concatenated output CANNOT regenerate original file names nor directory structure. \n") +} - /* multi-input into single output : --rm is not allowed */ - 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 (action == FIO_RUST_MULTI_FILES_ACTION_PROCEED) return 0; +static void +FIO_multiFilesConcatDisableRemove(void* opaque) +{ + FIO_multiFilesConcatCallbackContext_t* const context = + (FIO_multiFilesConcatCallbackContext_t*)opaque; + DISPLAYLEVEL(2, "Since it's a destructive operation, input files will not be removed. \n"); + context->prefs->removeSrcFile = 0; +} +static void +FIO_multiFilesConcatDisplayQuietAbort(void* opaque) +{ + (void)opaque; /* multiple files concatenated into single destination file using -o without -f */ - 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); + /* quiet mode => no prompt => fail automatically */ + DISPLAYLEVEL(1, "Concatenating multiple processed inputs into a single output loses file metadata. \n"); + DISPLAYLEVEL(1, "Aborting. \n"); +} + +static int +FIO_multiFilesConcatRequireConfirmation(void* opaque, int hasStdinInput) +{ + (void)opaque; + return UTIL_requireUserConfirmation("Proceed? (y/n): ", "Aborting...", "yY", hasStdinInput); +} + +static int FIO_multiFilesConcatWarning(const FIO_ctx_t* fCtx, FIO_prefs_t* prefs, const char* outFileName, int displayLevelCutoff) +{ + FIO_multiFilesConcatCallbackContext_t context = { fCtx, prefs }; + FIO_rust_multi_files_concat_callbacks_t callbacks; + callbacks.opaque = &context; + callbacks.fatalStdoutRemove = FIO_multiFilesConcatFatalStdoutRemove; + callbacks.fatalTestRemove = FIO_multiFilesConcatFatalTestRemove; + callbacks.displayWarning = FIO_multiFilesConcatDisplayWarning; + callbacks.disableRemove = FIO_multiFilesConcatDisableRemove; + callbacks.displayQuietAbort = FIO_multiFilesConcatDisplayQuietAbort; + callbacks.requireConfirmation = FIO_multiFilesConcatRequireConfirmation; + return FIO_rust_multiFilesConcatWarning( + fCtx->nbFilesTotal, fCtx->hasStdoutOutput, prefs->testMode, + outFileName != NULL, prefs->removeSrcFile, prefs->overwrite, + g_display_prefs.displayLevel, displayLevelCutoff, fCtx->hasStdinInput, + outFileName, &callbacks); } #ifndef ZSTD_NOCOMPRESS diff --git a/rust/src/fileio_prefs.rs b/rust/src/fileio_prefs.rs index e3a513ed8..c6f878452 100644 --- a/rust/src/fileio_prefs.rs +++ b/rust/src/fileio_prefs.rs @@ -2202,6 +2202,151 @@ fn multi_files_concat_action( } } +pub type FIO_multiFilesConcatFatalFn = unsafe extern "C" fn(*mut c_void); +pub type FIO_multiFilesConcatWarningFn = unsafe extern "C" fn(*mut c_void, c_int, *const c_char); +pub type FIO_multiFilesConcatDisableRemoveFn = unsafe extern "C" fn(*mut c_void); +pub type FIO_multiFilesConcatQuietAbortFn = unsafe extern "C" fn(*mut c_void); +pub type FIO_multiFilesConcatConfirmationFn = unsafe extern "C" fn(*mut c_void, c_int) -> c_int; + +/// C-owned diagnostics, preference mutation, and prompt callbacks for the +/// multiple-input concatenation policy. +#[repr(C)] +pub struct FIO_multiFilesConcatCallbacks_t { + opaque: *mut c_void, + fatalStdoutRemove: Option, + fatalTestRemove: Option, + displayWarning: Option, + disableRemove: Option, + displayQuietAbort: Option, + requireConfirmation: Option, +} + +const _: () = { + assert!(size_of::>() == size_of::()); + assert!(size_of::>() == size_of::()); + assert!(size_of::>() == size_of::()); + assert!(size_of::>() == size_of::()); + assert!(size_of::>() == size_of::()); + assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, opaque) == 0); + assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, fatalStdoutRemove) == size_of::()); + assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, fatalTestRemove) == 2 * size_of::()); + assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, displayWarning) == 3 * size_of::()); + assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, disableRemove) == 4 * size_of::()); + assert!( + offset_of!(FIO_multiFilesConcatCallbacks_t, displayQuietAbort) == 5 * size_of::() + ); + assert!( + offset_of!(FIO_multiFilesConcatCallbacks_t, requireConfirmation) == 6 * size_of::() + ); + assert!(size_of::() == 7 * size_of::()); +}; + +/// Run the policy/order around multiple inputs sharing one output. +/// +/// Rust owns only the scalar action sequence, including the reclassification +/// after `--rm` is disabled. C callbacks retain exact diagnostics, the +/// private preference mutation, the prompt, and fatal exit behavior. +#[allow(clippy::too_many_arguments)] +#[no_mangle] +pub unsafe extern "C" fn FIO_rust_multiFilesConcatWarning( + nb_files_total: c_int, + has_stdout_output: c_int, + test_mode: c_int, + has_output_file: c_int, + remove_src_file: c_int, + overwrite: c_int, + display_level: c_int, + display_level_cutoff: c_int, + has_stdin_input: c_int, + out_file_name: *const c_char, + callbacks: *const FIO_multiFilesConcatCallbacks_t, +) -> c_int { + assert!(!callbacks.is_null()); + let callbacks = unsafe { &*callbacks }; + let action = multi_files_concat_action( + nb_files_total, + has_stdout_output, + test_mode, + has_output_file, + remove_src_file, + overwrite, + display_level, + display_level_cutoff, + ); + + match action { + FIO_MULTI_FILES_ACTION_FATAL_STDOUT_REMOVE => { + let fatal = callbacks + .fatalStdoutRemove + .expect("multi-file stdout-removal callback is required"); + unsafe { fatal(callbacks.opaque) }; + return 1; + } + FIO_MULTI_FILES_ACTION_FATAL_TEST_REMOVE => { + let fatal = callbacks + .fatalTestRemove + .expect("multi-file test-removal callback is required"); + unsafe { fatal(callbacks.opaque) }; + return 1; + } + _ => {} + } + + if nb_files_total == 1 { + return 0; + } + debug_assert!(nb_files_total > 1); + + if out_file_name.is_null() || test_mode != 0 { + return 0; + } + + let display_warning = callbacks + .displayWarning + .expect("multi-file warning callback is required"); + unsafe { display_warning(callbacks.opaque, has_stdout_output, out_file_name) }; + + let action = if action == FIO_MULTI_FILES_ACTION_DISABLE_REMOVE { + let disable_remove = callbacks + .disableRemove + .expect("multi-file remove-disable callback is required"); + unsafe { disable_remove(callbacks.opaque) }; + multi_files_concat_action( + nb_files_total, + has_stdout_output, + test_mode, + 1, + 0, + overwrite, + display_level, + display_level_cutoff, + ) + } else { + action + }; + + match action { + FIO_MULTI_FILES_ACTION_PROCEED => 0, + FIO_MULTI_FILES_ACTION_QUIET_ABORT => { + let display_quiet_abort = callbacks + .displayQuietAbort + .expect("multi-file quiet-abort callback is required"); + unsafe { display_quiet_abort(callbacks.opaque) }; + 1 + } + FIO_MULTI_FILES_ACTION_CONFIRM => { + let require_confirmation = callbacks + .requireConfirmation + .expect("multi-file confirmation callback is required"); + unsafe { require_confirmation(callbacks.opaque, has_stdin_input) } + } + FIO_MULTI_FILES_ACTION_FATAL_STDOUT_REMOVE | FIO_MULTI_FILES_ACTION_FATAL_TEST_REMOVE => { + unreachable!("remove-free multi-file action cannot be fatal") + } + _ => unreachable!("unknown multi-file concatenation action {action}"), + } +} + /// Select the warning/prompt action for multiple inputs sharing one output. /// /// C retains the user-facing diagnostics, preference mutation, and prompt; @@ -3872,6 +4017,168 @@ mod tests { ); } + #[derive(Default)] + struct MultiFilesConcatTestState { + events: Vec<&'static str>, + warning_stdout: c_int, + warning_name: Vec, + confirmation_has_stdin: c_int, + confirmation_result: c_int, + } + + unsafe extern "C" fn multi_files_concat_fatal_stdout_test(context: *mut c_void) { + let state = unsafe { &mut *context.cast::() }; + state.events.push("fatal-stdout"); + } + + unsafe extern "C" fn multi_files_concat_fatal_test_test(context: *mut c_void) { + let state = unsafe { &mut *context.cast::() }; + state.events.push("fatal-test"); + } + + unsafe extern "C" fn multi_files_concat_warning_test( + context: *mut c_void, + has_stdout_output: c_int, + out_file_name: *const c_char, + ) { + let state = unsafe { &mut *context.cast::() }; + state.events.push("warning"); + state.warning_stdout = has_stdout_output; + state.warning_name = unsafe { CStr::from_ptr(out_file_name) }.to_bytes().to_vec(); + } + + unsafe extern "C" fn multi_files_concat_disable_remove_test(context: *mut c_void) { + let state = unsafe { &mut *context.cast::() }; + state.events.push("disable-remove"); + } + + unsafe extern "C" fn multi_files_concat_quiet_abort_test(context: *mut c_void) { + let state = unsafe { &mut *context.cast::() }; + state.events.push("quiet-abort"); + } + + unsafe extern "C" fn multi_files_concat_confirmation_test( + context: *mut c_void, + has_stdin_input: c_int, + ) -> c_int { + let state = unsafe { &mut *context.cast::() }; + state.events.push("confirmation"); + state.confirmation_has_stdin = has_stdin_input; + state.confirmation_result + } + + fn multi_files_concat_callbacks( + state: &mut MultiFilesConcatTestState, + ) -> FIO_multiFilesConcatCallbacks_t { + FIO_multiFilesConcatCallbacks_t { + opaque: (state as *mut MultiFilesConcatTestState).cast(), + fatalStdoutRemove: Some(multi_files_concat_fatal_stdout_test), + fatalTestRemove: Some(multi_files_concat_fatal_test_test), + displayWarning: Some(multi_files_concat_warning_test), + disableRemove: Some(multi_files_concat_disable_remove_test), + displayQuietAbort: Some(multi_files_concat_quiet_abort_test), + requireConfirmation: Some(multi_files_concat_confirmation_test), + } + } + + #[test] + fn multi_files_concat_wrapper_preserves_warning_disable_recheck_order() { + let output = CString::new("output.zst").unwrap(); + let mut state = MultiFilesConcatTestState { + confirmation_result: 0, + ..MultiFilesConcatTestState::default() + }; + let callbacks = multi_files_concat_callbacks(&mut state); + + let result = unsafe { + FIO_rust_multiFilesConcatWarning(3, 0, 0, 1, 1, 0, 2, 1, 1, output.as_ptr(), &callbacks) + }; + + assert_eq!(result, 0); + assert_eq!(state.events, ["warning", "disable-remove", "confirmation"]); + assert_eq!(state.warning_stdout, 0); + assert_eq!(state.warning_name, b"output.zst"); + assert_eq!(state.confirmation_has_stdin, 1); + } + + #[test] + fn multi_files_concat_wrapper_keeps_fatal_and_quiet_callbacks_on_their_paths() { + let output = CString::new("output.zst").unwrap(); + + let mut state = MultiFilesConcatTestState::default(); + let callbacks = multi_files_concat_callbacks(&mut state); + let result = unsafe { + FIO_rust_multiFilesConcatWarning(3, 1, 0, 1, 1, 0, 2, 1, 0, output.as_ptr(), &callbacks) + }; + assert_eq!(result, 1); + assert_eq!(state.events, ["fatal-stdout"]); + + let mut state = MultiFilesConcatTestState::default(); + let callbacks = multi_files_concat_callbacks(&mut state); + let result = unsafe { + FIO_rust_multiFilesConcatWarning(3, 0, 1, 1, 1, 0, 2, 1, 0, output.as_ptr(), &callbacks) + }; + assert_eq!(result, 1); + assert_eq!(state.events, ["fatal-test"]); + + let mut state = MultiFilesConcatTestState::default(); + let callbacks = multi_files_concat_callbacks(&mut state); + let result = unsafe { + FIO_rust_multiFilesConcatWarning(3, 0, 0, 1, 0, 0, 1, 1, 0, output.as_ptr(), &callbacks) + }; + assert_eq!(result, 1); + assert_eq!(state.events, ["warning", "quiet-abort"]); + } + + #[test] + fn multi_files_concat_callback_bridge_matches_c_layout() { + let word = size_of::<*mut c_void>(); + let callback = size_of::>(); + + assert_eq!(size_of::>(), callback); + assert_eq!( + size_of::>(), + callback + ); + assert_eq!( + size_of::>(), + callback + ); + assert_eq!( + size_of::>(), + callback + ); + assert_eq!(offset_of!(FIO_multiFilesConcatCallbacks_t, opaque), 0); + assert_eq!( + offset_of!(FIO_multiFilesConcatCallbacks_t, fatalStdoutRemove), + word + ); + assert_eq!( + offset_of!(FIO_multiFilesConcatCallbacks_t, fatalTestRemove), + word + callback + ); + assert_eq!( + offset_of!(FIO_multiFilesConcatCallbacks_t, displayWarning), + word + 2 * callback + ); + assert_eq!( + offset_of!(FIO_multiFilesConcatCallbacks_t, disableRemove), + word + 3 * callback + ); + assert_eq!( + offset_of!(FIO_multiFilesConcatCallbacks_t, displayQuietAbort), + word + 4 * callback + ); + assert_eq!( + offset_of!(FIO_multiFilesConcatCallbacks_t, requireConfirmation), + word + 5 * callback + ); + assert_eq!( + size_of::(), + word + 6 * callback + ); + } + #[test] fn buffer_shims_preserve_fields_and_c_layout() { let input_word = size_of::<*const c_void>();