refactor(cli): move destination action policy to Rust

FIO_openDstFile retained the filesystem leaf in Rust but its C wrapper still encoded the status precedence for test mode, stdout, same-file protection, sparse-mode adjustment, overwrite prompting, removal, retry, and final errors. Centralize that pure action classification in Rust without moving FILE* handling, metadata, preference mutation, prompts, diagnostics, or retry callbacks across the ABI.\n\nThe action bridge validates status, confirmation, and sparse-state inputs and has focused tests for every destination outcome, sparse-first ordering, prompt acceptance/abort, quiet mode, overwrite mode, setvbuf/open failures, success, and invalid inputs.\n\nTest Plan:\n- ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo fmt/clippy gates passed before staging\n- git diff --cached --check\n- Full capped native and original-test verification follows after the batch is committed.
This commit is contained in:
2026-07-21 06:28:58 +02:00
parent db5ae46f42
commit c075a7b2d9
2 changed files with 390 additions and 40 deletions
+315 -4
View File
@@ -7,10 +7,11 @@
//! 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
//! existing messages, policy fields, and conventions. Rust also classifies
//! dictionary-loader statuses into diagnostic actions; C retains the
//! metadata and exact diagnostic text while the existing loader leaves
//! continue to publish their narrow status ABI.
//! existing messages, policy fields, and conventions. Rust also owns the
//! pure destination status/action ordering and classifies dictionary-loader
//! statuses into diagnostic actions; C retains the metadata and exact
//! diagnostic text while the existing loader leaves continue to publish their
//! narrow status ABI.
use std::ffi::{c_char, c_void, CStr};
use std::fs::File;
@@ -64,6 +65,23 @@ const FIO_OPEN_DST_EXISTING: c_int = 5;
const FIO_OPEN_DST_NULL_DEVICE_REGULAR: c_int = 6;
const FIO_OPEN_DST_OPEN_FAILED: c_int = 7;
const FIO_DESTINATION_BUFFER_SIZE: usize = 1 << 20;
const FIO_OPEN_DST_CONFIRMATION_UNASKED: c_int = -1;
const FIO_OPEN_DST_CONFIRMATION_ACCEPT: c_int = 0;
const FIO_OPEN_DST_CONFIRMATION_ABORT: c_int = 1;
const FIO_OPEN_DST_ACTION_TEST_MODE: c_int = 0;
const FIO_OPEN_DST_ACTION_STDOUT: c_int = 1;
const FIO_OPEN_DST_ACTION_SAME_FILE: c_int = 2;
const FIO_OPEN_DST_ACTION_ADJUST_SPARSE: c_int = 3;
const FIO_OPEN_DST_ACTION_NULL_DEVICE_REGULAR: c_int = 4;
const FIO_OPEN_DST_ACTION_EXISTING_QUIET_ABORT: c_int = 5;
const FIO_OPEN_DST_ACTION_EXISTING_PROMPT: c_int = 6;
const FIO_OPEN_DST_ACTION_EXISTING_ABORT: c_int = 7;
const FIO_OPEN_DST_ACTION_EXISTING_REMOVE: c_int = 8;
const FIO_OPEN_DST_ACTION_OPEN_FAILED: c_int = 9;
const FIO_OPEN_DST_ACTION_SETBUF_FAILED: c_int = 10;
const FIO_OPEN_DST_ACTION_SUCCESS: c_int = 11;
const FIO_OPEN_DST_ACTION_INVALID: c_int = 12;
static STDOUT_MARK: &[u8] = b"/*stdout*\\\0";
@@ -103,6 +121,132 @@ fn classify_destination(
DestinationDecision::Create
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DestinationAction {
TestMode,
Stdout,
SameFile,
AdjustSparse,
NullDeviceRegular,
ExistingQuietAbort,
ExistingPrompt,
ExistingAbort,
ExistingRemove,
OpenFailed,
SetbufFailed,
Success,
Invalid,
}
impl DestinationAction {
fn code(self) -> c_int {
match self {
Self::TestMode => FIO_OPEN_DST_ACTION_TEST_MODE,
Self::Stdout => FIO_OPEN_DST_ACTION_STDOUT,
Self::SameFile => FIO_OPEN_DST_ACTION_SAME_FILE,
Self::AdjustSparse => FIO_OPEN_DST_ACTION_ADJUST_SPARSE,
Self::NullDeviceRegular => FIO_OPEN_DST_ACTION_NULL_DEVICE_REGULAR,
Self::ExistingQuietAbort => FIO_OPEN_DST_ACTION_EXISTING_QUIET_ABORT,
Self::ExistingPrompt => FIO_OPEN_DST_ACTION_EXISTING_PROMPT,
Self::ExistingAbort => FIO_OPEN_DST_ACTION_EXISTING_ABORT,
Self::ExistingRemove => FIO_OPEN_DST_ACTION_EXISTING_REMOVE,
Self::OpenFailed => FIO_OPEN_DST_ACTION_OPEN_FAILED,
Self::SetbufFailed => FIO_OPEN_DST_ACTION_SETBUF_FAILED,
Self::Success => FIO_OPEN_DST_ACTION_SUCCESS,
Self::Invalid => FIO_OPEN_DST_ACTION_INVALID,
}
}
}
fn is_known_destination_status(status: c_int) -> bool {
matches!(
status,
FIO_OPEN_DST_SUCCESS
| FIO_OPEN_DST_SETBUF_FAILED
| FIO_OPEN_DST_TEST_MODE
| FIO_OPEN_DST_STDOUT
| FIO_OPEN_DST_SAME_FILE
| FIO_OPEN_DST_EXISTING
| FIO_OPEN_DST_NULL_DEVICE_REGULAR
| FIO_OPEN_DST_OPEN_FAILED
)
}
/// Selects the next destination-opening action without touching filesystem or
/// 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
/// ordering between those operations is centralized here.
fn destination_action(
status: c_int,
overwrite: c_int,
display_level: c_int,
confirmation: c_int,
sparse_adjusted: c_int,
) -> DestinationAction {
if !is_known_destination_status(status)
|| (sparse_adjusted != 0 && sparse_adjusted != 1)
{
DestinationAction::Invalid
} else if sparse_adjusted == 0
&& !matches!(
status,
FIO_OPEN_DST_TEST_MODE | FIO_OPEN_DST_STDOUT | FIO_OPEN_DST_SAME_FILE
)
{
DestinationAction::AdjustSparse
} else {
match status {
FIO_OPEN_DST_TEST_MODE => DestinationAction::TestMode,
FIO_OPEN_DST_STDOUT => DestinationAction::Stdout,
FIO_OPEN_DST_SAME_FILE => DestinationAction::SameFile,
FIO_OPEN_DST_NULL_DEVICE_REGULAR => DestinationAction::NullDeviceRegular,
FIO_OPEN_DST_EXISTING => {
if overwrite != 0 {
DestinationAction::ExistingRemove
} else if display_level <= 1 {
DestinationAction::ExistingQuietAbort
} else {
match confirmation {
FIO_OPEN_DST_CONFIRMATION_UNASKED => DestinationAction::ExistingPrompt,
FIO_OPEN_DST_CONFIRMATION_ACCEPT => DestinationAction::ExistingRemove,
FIO_OPEN_DST_CONFIRMATION_ABORT => DestinationAction::ExistingAbort,
_ => DestinationAction::Invalid,
}
}
}
FIO_OPEN_DST_OPEN_FAILED => DestinationAction::OpenFailed,
FIO_OPEN_DST_SETBUF_FAILED => DestinationAction::SetbufFailed,
FIO_OPEN_DST_SUCCESS => DestinationAction::Success,
_ => DestinationAction::Invalid,
}
}
}
/// Exposes the pure destination status/action policy to the C adapter.
#[no_mangle]
pub extern "C" fn FIO_rust_openDstFileAction(
status: c_int,
overwrite: c_int,
display_level: c_int,
confirmation: c_int,
sparse_adjusted: c_int,
) -> c_int {
destination_action(
status,
overwrite,
display_level,
confirmation,
sparse_adjusted,
)
.code()
}
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;
@@ -891,6 +1035,173 @@ mod tests {
);
}
#[test]
fn destination_open_action_covers_status_ordering_and_confirmation_branches() {
let cases = [
(
FIO_OPEN_DST_TEST_MODE,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
0,
DestinationAction::TestMode,
),
(
FIO_OPEN_DST_STDOUT,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
0,
DestinationAction::Stdout,
),
(
FIO_OPEN_DST_SAME_FILE,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
0,
DestinationAction::SameFile,
),
(
FIO_OPEN_DST_NULL_DEVICE_REGULAR,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
0,
DestinationAction::AdjustSparse,
),
(
FIO_OPEN_DST_NULL_DEVICE_REGULAR,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
1,
DestinationAction::NullDeviceRegular,
),
(
FIO_OPEN_DST_EXISTING,
0,
1,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
1,
DestinationAction::ExistingQuietAbort,
),
(
FIO_OPEN_DST_EXISTING,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
1,
DestinationAction::ExistingPrompt,
),
(
FIO_OPEN_DST_EXISTING,
0,
2,
FIO_OPEN_DST_CONFIRMATION_ABORT,
1,
DestinationAction::ExistingAbort,
),
(
FIO_OPEN_DST_EXISTING,
0,
2,
FIO_OPEN_DST_CONFIRMATION_ACCEPT,
1,
DestinationAction::ExistingRemove,
),
(
FIO_OPEN_DST_EXISTING,
1,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
1,
DestinationAction::ExistingRemove,
),
(
FIO_OPEN_DST_OPEN_FAILED,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
1,
DestinationAction::OpenFailed,
),
(
FIO_OPEN_DST_SETBUF_FAILED,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
1,
DestinationAction::SetbufFailed,
),
(
FIO_OPEN_DST_SUCCESS,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
1,
DestinationAction::Success,
),
];
for (status, overwrite, display_level, confirmation, sparse_adjusted, expected) in cases {
assert_eq!(
destination_action(
status,
overwrite,
display_level,
confirmation,
sparse_adjusted,
),
expected,
"unexpected destination action for status {status}"
);
assert_eq!(
FIO_rust_openDstFileAction(
status,
overwrite,
display_level,
confirmation,
sparse_adjusted,
),
expected.code(),
"C action ABI disagrees for status {status}"
);
}
}
#[test]
fn destination_open_action_rejects_invalid_policy_inputs() {
assert_eq!(
destination_action(
c_int::MAX,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
0,
),
DestinationAction::Invalid
);
assert_eq!(
destination_action(
FIO_OPEN_DST_SUCCESS,
0,
2,
FIO_OPEN_DST_CONFIRMATION_UNASKED,
2,
),
DestinationAction::Invalid
);
assert_eq!(
destination_action(FIO_OPEN_DST_EXISTING, 0, 2, c_int::MAX, 1),
DestinationAction::Invalid
);
assert_eq!(
FIO_rust_openDstFileAction(c_int::MAX, 0, 2, -1, 0),
FIO_OPEN_DST_ACTION_INVALID
);
}
#[test]
fn dictionary_loader_diagnostics_preserve_each_backend_status_class() {
let cases = [