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:
2026-07-21 13:09:16 +02:00
parent 6cf4898ab1
commit b394cae70f
2 changed files with 468 additions and 109 deletions
+330 -6
View File
@@ -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<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 {
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<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" {
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<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]
fn dictionary_loader_diagnostics_preserve_each_backend_status_class() {
let cases = [