refactor(fileio): move list-file ordering to Rust

Move the single-file --list status gates, diagnostic-versus-metadata ordering, and aggregate projection into Rust. C retains file opening and frame analysis, the private fileInfo_t layout, exact diagnostics, and row formatting behind explicit callbacks, so the user-visible behavior remains unchanged while the policy boundary is auditable.

Test Plan:

- git diff --cached --check

- worker format, check, clippy, and serial C compilation (passed); focused Rust tests compiled but the standalone link hit the pre-existing C bridge-symbol gap
This commit is contained in:
2026-07-21 09:48:48 +02:00
parent 9c7bf99a61
commit 2e5f7308c8
2 changed files with 372 additions and 44 deletions
+294 -2
View File
@@ -241,6 +241,7 @@ pub struct FIO_compressionParameters {
/// C's private `fileInfo_t` from `programs/fileio.c`.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FIO_fileInfo_t {
decompressedSize: u64,
compressedSize: u64,
@@ -255,8 +256,8 @@ pub struct FIO_fileInfo_t {
}
/// The subset of C's private `fileInfo_t` needed by the `--list` aggregate
/// row. C keeps opening, parsing, and formatting each file; Rust owns only
/// the policy that iterates and aggregates these values.
/// row. C keeps opening, parsing, and formatting each file; Rust owns the
/// per-file and multi-file policy around those callbacks.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FIO_listFileInfoProjection_t {
@@ -729,6 +730,33 @@ pub type FIO_listMultipleFilesListFileFn = unsafe extern "C" fn(
pub type FIO_listMultipleFilesDisplayTotalFn =
unsafe extern "C" fn(*mut c_void, *const FIO_listFileInfoProjection_t);
pub type FIO_listFileGetInfoFn = unsafe extern "C" fn(
*mut c_void,
*const c_char,
*mut FIO_fileInfo_t,
) -> c_int;
pub type FIO_listFileDisplayStatusFn =
unsafe extern "C" fn(*mut c_void, c_int, *const c_char, c_int);
pub type FIO_listFileDisplayInfoFn = unsafe extern "C" fn(
*mut c_void,
*const c_char,
*const FIO_fileInfo_t,
c_int,
);
/// Callbacks for the C-owned `--list` file operation.
///
/// The callbacks keep stat/open, frame analysis, exact status diagnostics, and
/// row formatting in C. Rust only orders those operations and publishes the
/// small aggregate projection consumed by the surrounding multi-file policy.
#[repr(C)]
pub struct FIO_listFileCallbacks_t {
opaque: *mut c_void,
getInfo: Option<FIO_listFileGetInfoFn>,
displayStatus: Option<FIO_listFileDisplayStatusFn>,
displayInfo: Option<FIO_listFileDisplayInfoFn>,
}
#[inline]
fn list_file_status_action(status: c_int) -> c_int {
match status {
@@ -750,6 +778,85 @@ pub extern "C" fn FIO_rust_listFileStatusAction(status: c_int) -> c_int {
list_file_status_action(status)
}
#[inline]
fn project_list_file_info(info: &FIO_fileInfo_t) -> FIO_listFileInfoProjection_t {
FIO_listFileInfoProjection_t {
decompressedSize: info.decompressedSize,
compressedSize: info.compressedSize,
numActualFrames: info.numActualFrames,
numSkippableFrames: info.numSkippableFrames,
decompUnavailable: info.decompUnavailable,
usesCheck: info.usesCheck,
nbFiles: info.nbFiles,
}
}
/// Run the policy and ordering around one C-owned `--list` file operation.
///
/// C retains the stat/open and frame-analysis callback, all user-visible
/// status diagnostics, and the full metadata row formatter. Rust preserves
/// the original status gates: fatal statuses are reported and return `1`, a
/// frame error reports a diagnostic but still formats and publishes its
/// partial metadata, and success formats and publishes normally.
#[no_mangle]
pub unsafe extern "C" fn FIO_rust_listFile(
input_name: *const c_char,
display_level: c_int,
output: *mut FIO_listFileInfoProjection_t,
callbacks: *const FIO_listFileCallbacks_t,
) -> c_int {
assert!(!input_name.is_null());
assert!(!output.is_null());
assert!(!callbacks.is_null());
let callbacks = unsafe { &*callbacks };
let get_info = callbacks
.getInfo
.expect("--list file-info callback is required");
let display_status = callbacks
.displayStatus
.expect("--list status-display callback is required");
let display_info = callbacks
.displayInfo
.expect("--list info-display callback is required");
let mut info = unsafe { std::mem::zeroed::<FIO_fileInfo_t>() };
let status = unsafe { get_info(callbacks.opaque, input_name, &mut info) };
let action = list_file_status_action(status);
match action {
FIO_RUST_LIST_FILE_ACTION_FRAME_ERROR
| FIO_RUST_LIST_FILE_ACTION_NOT_ZSTD
| FIO_RUST_LIST_FILE_ACTION_FILE_ERROR
| FIO_RUST_LIST_FILE_ACTION_TRUNCATED_INPUT => unsafe {
display_status(callbacks.opaque, action, input_name, display_level);
},
FIO_RUST_LIST_FILE_ACTION_SUCCESS | FIO_RUST_LIST_FILE_ACTION_INVALID => {}
_ => unreachable!("unknown --list file action {action}"),
}
match action {
FIO_RUST_LIST_FILE_ACTION_NOT_ZSTD
| FIO_RUST_LIST_FILE_ACTION_FILE_ERROR
| FIO_RUST_LIST_FILE_ACTION_TRUNCATED_INPUT => 1,
FIO_RUST_LIST_FILE_ACTION_SUCCESS | FIO_RUST_LIST_FILE_ACTION_FRAME_ERROR => {
unsafe { display_info(callbacks.opaque, input_name, &info, display_level) };
unsafe { output.write(project_list_file_info(&info)) };
status
}
FIO_RUST_LIST_FILE_ACTION_INVALID => {
unsafe { display_info(callbacks.opaque, input_name, &info, display_level) };
unsafe { output.write(project_list_file_info(&info)) };
debug_assert!(
action == FIO_RUST_LIST_FILE_ACTION_SUCCESS
|| action == FIO_RUST_LIST_FILE_ACTION_FRAME_ERROR
);
status
}
_ => unreachable!("unknown --list file action {action}"),
}
}
#[inline]
fn remove_file_action(status: c_int) -> c_int {
match status {
@@ -3296,6 +3403,57 @@ mod tests {
}
}
#[derive(Default)]
struct ListFileTestState {
status: c_int,
info: FIO_fileInfo_t,
events: Vec<&'static str>,
status_actions: Vec<(c_int, c_int)>,
displayed: Option<(FIO_fileInfo_t, c_int)>,
}
unsafe extern "C" fn list_file_get_info(
opaque: *mut c_void,
_file_name: *const c_char,
info: *mut FIO_fileInfo_t,
) -> c_int {
let state = unsafe { &mut *opaque.cast::<ListFileTestState>() };
state.events.push("get-info");
unsafe { info.write(state.info) };
state.status
}
unsafe extern "C" fn list_file_display_status(
opaque: *mut c_void,
action: c_int,
_file_name: *const c_char,
display_level: c_int,
) {
let state = unsafe { &mut *opaque.cast::<ListFileTestState>() };
state.events.push("display-status");
state.status_actions.push((action, display_level));
}
unsafe extern "C" fn list_file_display_info(
opaque: *mut c_void,
_file_name: *const c_char,
info: *const FIO_fileInfo_t,
display_level: c_int,
) {
let state = unsafe { &mut *opaque.cast::<ListFileTestState>() };
state.events.push("display-info");
state.displayed = Some((unsafe { *info }, display_level));
}
fn list_file_callbacks(state: &mut ListFileTestState) -> FIO_listFileCallbacks_t {
FIO_listFileCallbacks_t {
opaque: (state as *mut ListFileTestState).cast(),
getInfo: Some(list_file_get_info),
displayStatus: Some(list_file_display_status),
displayInfo: Some(list_file_display_info),
}
}
#[cfg(feature = "decompression")]
struct TemporaryCFile(*mut libc::FILE);
@@ -4031,6 +4189,140 @@ mod tests {
);
}
#[test]
fn list_file_policy_preserves_status_order_and_projection_gates() {
let file_name = CString::new("input.zst").unwrap();
let info = FIO_fileInfo_t {
decompressedSize: 100,
compressedSize: 40,
windowSize: 32,
numActualFrames: 2,
numSkippableFrames: 1,
decompUnavailable: 0,
usesCheck: 1,
checksum: [1, 2, 3, 4],
nbFiles: 1,
dictID: 7,
};
let expected_projection = project_list_file_info(&info);
let display_level = 3;
for (status, expected_action, expected_return, publishes_info) in [
(
FIO_LIST_INFO_SUCCESS,
FIO_RUST_LIST_FILE_ACTION_SUCCESS,
0,
true,
),
(
FIO_LIST_INFO_FRAME_ERROR,
FIO_RUST_LIST_FILE_ACTION_FRAME_ERROR,
1,
true,
),
(
FIO_LIST_INFO_NOT_ZSTD,
FIO_RUST_LIST_FILE_ACTION_NOT_ZSTD,
1,
false,
),
(
FIO_LIST_INFO_FILE_ERROR,
FIO_RUST_LIST_FILE_ACTION_FILE_ERROR,
1,
false,
),
(
FIO_LIST_INFO_TRUNCATED_INPUT,
FIO_RUST_LIST_FILE_ACTION_TRUNCATED_INPUT,
1,
false,
),
] {
let mut state = ListFileTestState {
status,
info,
..ListFileTestState::default()
};
let callbacks = list_file_callbacks(&mut state);
let sentinel = FIO_listFileInfoProjection_t {
decompressedSize: 9,
compressedSize: 8,
numActualFrames: 7,
numSkippableFrames: 6,
decompUnavailable: 5,
usesCheck: 4,
nbFiles: 3,
};
let mut output = sentinel;
let result = unsafe {
FIO_rust_listFile(
file_name.as_ptr(),
display_level,
&mut output,
&callbacks,
)
};
assert_eq!(result, expected_return, "status {status}");
assert_eq!(
state.events,
if publishes_info {
if expected_action == FIO_RUST_LIST_FILE_ACTION_FRAME_ERROR {
vec!["get-info", "display-status", "display-info"]
} else {
vec!["get-info", "display-info"]
}
} else {
vec!["get-info", "display-status"]
},
"status {status} event order"
);
assert_eq!(
state.status_actions,
if expected_action == FIO_RUST_LIST_FILE_ACTION_SUCCESS {
Vec::new()
} else {
vec![(expected_action, display_level)]
},
"status {status} diagnostic action"
);
assert_eq!(
state.displayed,
publishes_info.then_some((info, display_level)),
"status {status} display gate"
);
assert_eq!(
output,
if publishes_info {
expected_projection
} else {
sentinel
},
"status {status} projection gate"
);
}
}
#[test]
fn list_file_callback_bridge_matches_c_layout() {
let word = size_of::<*mut c_void>();
let callback = size_of::<Option<FIO_listFileGetInfoFn>>();
assert_eq!(offset_of!(FIO_listFileCallbacks_t, opaque), 0);
assert_eq!(offset_of!(FIO_listFileCallbacks_t, getInfo), word);
assert_eq!(
offset_of!(FIO_listFileCallbacks_t, displayStatus),
word + callback
);
assert_eq!(
offset_of!(FIO_listFileCallbacks_t, displayInfo),
word + 2 * callback
);
assert_eq!(size_of::<FIO_listFileCallbacks_t>(), word + 3 * callback);
}
#[test]
fn file_info_layout_matches_private_c_record() {
let word = size_of::<u64>();