refactor(fileio): move concatenation policy ordering to Rust

Move the multi-input single-output decision sequence out of
FIO_multiFilesConcatWarning while preserving the C-owned CLI boundary. The
previous wrapper classified fatal remove cases, emitted the concatenation
warning, disabled --rm, reclassified the action, and then selected quiet
abort or confirmation in C. Add a repr(C) callback projection so Rust owns
only that scalar ordering while C continues to own exact diagnostics, the
confirmation prompt, FIO_prefs_t mutation, and all private state.

The Rust bridge re-runs the action after the C disable-remove callback with
the same has-output/remove arguments as the original wrapper. C and Rust
assert the callback layout, and focused tests cover callback order,
fatal/quiet paths, and ABI offsets. Existing scalar action tests remain in
place.

Test Plan:
- `ulimit -v 41943040; CARGO_BUILD_JOBS=1 make -B -C programs -j1 fileio.o`
  — passed; only pre-existing suffixList C++-compat warnings appeared.
- `ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo clippy --manifest-path
  rust/cli/Cargo.toml --tests --no-deps` — passed.
- `ulimit -v 41943040; CARGO_BUILD_JOBS=1 RUSTFLAGS='-C
  link-arg=/tmp/zstd_rust_test_bridges.o' cargo test --manifest-path
  rust/cli/Cargo.toml --lib 'fileio_prefs::tests::multi_files_concat_'`
  — 8 passed.
- `ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo build --manifest-path
  rust/cli/Cargo.toml --lib` — passed; the archive exports
  `FIO_rust_multiFilesConcatWarning`.
- `cargo +nightly fmt --manifest-path rust/cli/Cargo.toml -- --check` — not
  clean due pre-existing formatting drift in unchanged Rust code; no bulk
  formatting was applied.
- Full native/upstream/fuzzer suites were not run by request.
This commit is contained in:
2026-07-21 10:17:54 +02:00
parent af91fb23ab
commit 5948934bd9
2 changed files with 436 additions and 60 deletions
+307
View File
@@ -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<FIO_multiFilesConcatFatalFn>,
fatalTestRemove: Option<FIO_multiFilesConcatFatalFn>,
displayWarning: Option<FIO_multiFilesConcatWarningFn>,
disableRemove: Option<FIO_multiFilesConcatDisableRemoveFn>,
displayQuietAbort: Option<FIO_multiFilesConcatQuietAbortFn>,
requireConfirmation: Option<FIO_multiFilesConcatConfirmationFn>,
}
const _: () = {
assert!(size_of::<Option<FIO_multiFilesConcatFatalFn>>() == size_of::<usize>());
assert!(size_of::<Option<FIO_multiFilesConcatWarningFn>>() == size_of::<usize>());
assert!(size_of::<Option<FIO_multiFilesConcatDisableRemoveFn>>() == size_of::<usize>());
assert!(size_of::<Option<FIO_multiFilesConcatQuietAbortFn>>() == size_of::<usize>());
assert!(size_of::<Option<FIO_multiFilesConcatConfirmationFn>>() == size_of::<usize>());
assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, opaque) == 0);
assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, fatalStdoutRemove) == size_of::<usize>());
assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, fatalTestRemove) == 2 * size_of::<usize>());
assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, displayWarning) == 3 * size_of::<usize>());
assert!(offset_of!(FIO_multiFilesConcatCallbacks_t, disableRemove) == 4 * size_of::<usize>());
assert!(
offset_of!(FIO_multiFilesConcatCallbacks_t, displayQuietAbort) == 5 * size_of::<usize>()
);
assert!(
offset_of!(FIO_multiFilesConcatCallbacks_t, requireConfirmation) == 6 * size_of::<usize>()
);
assert!(size_of::<FIO_multiFilesConcatCallbacks_t>() == 7 * size_of::<usize>());
};
/// 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<u8>,
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::<MultiFilesConcatTestState>() };
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::<MultiFilesConcatTestState>() };
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::<MultiFilesConcatTestState>() };
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::<MultiFilesConcatTestState>() };
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::<MultiFilesConcatTestState>() };
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::<MultiFilesConcatTestState>() };
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::<Option<FIO_multiFilesConcatFatalFn>>();
assert_eq!(size_of::<Option<FIO_multiFilesConcatWarningFn>>(), callback);
assert_eq!(
size_of::<Option<FIO_multiFilesConcatDisableRemoveFn>>(),
callback
);
assert_eq!(
size_of::<Option<FIO_multiFilesConcatQuietAbortFn>>(),
callback
);
assert_eq!(
size_of::<Option<FIO_multiFilesConcatConfirmationFn>>(),
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::<FIO_multiFilesConcatCallbacks_t>(),
word + 6 * callback
);
}
#[test]
fn buffer_shims_preserve_fields_and_c_layout() {
let input_word = size_of::<*const c_void>();