feat(fileio): move filename collision checks to Rust

Implement basename extraction, bytewise sorting, collision reporting, and allocation failure handling in the Rust fileio preferences module. Keep programs/fileio.c as the stable ABI shim and preserve non-UTF-8 filenames without lossy conversion.

Test Plan:

- cargo test --manifest-path rust/cli/Cargo.toml --no-default-features --features cli,compression,decompression

- cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets --no-default-features --features cli,compression,decompression

- make -B -C tests -j2 test-cli-tests

- git diff --cached --check
This commit is contained in:
2026-07-18 03:19:57 +02:00
parent 96c7f3b900
commit 65bfc6bb2b
2 changed files with 119 additions and 30 deletions
+2 -29
View File
@@ -306,6 +306,7 @@ static int FIO_shouldDisplayMultipleFileSummary(FIO_ctx_t const* fCtx)
/* These symbols are implemented by rust/src/fileio_prefs.rs. The declarations
* in fileio.h remain the C ABI shims while all actual file I/O stays here. */
int FIO_rust_checkFilenameCollisions(const char** filenameTable, unsigned nbFiles);
/*-*************************************
@@ -678,35 +679,7 @@ static void FIO_initDict(FIO_Dict_t* dict, const char* fileName, FIO_prefs_t* co
* Checks for and warns if there are any files that would have the same output path
*/
int FIO_checkFilenameCollisions(const char** filenameTable, unsigned nbFiles) {
const char **filenameTableSorted, *prevElem, *filename;
unsigned u;
filenameTableSorted = (const char**) malloc(sizeof(char*) * nbFiles);
if (!filenameTableSorted) {
DISPLAYLEVEL(1, "Allocation error during filename collision checking \n");
return 1;
}
for (u = 0; u < nbFiles; ++u) {
filename = strrchr(filenameTable[u], PATH_SEP);
if (filename == NULL) {
filenameTableSorted[u] = filenameTable[u];
} else {
filenameTableSorted[u] = filename+1;
}
}
qsort((void*)filenameTableSorted, nbFiles, sizeof(char*), UTIL_compareStr);
prevElem = filenameTableSorted[0];
for (u = 1; u < nbFiles; ++u) {
if (strcmp(prevElem, filenameTableSorted[u]) == 0) {
DISPLAYLEVEL(2, "WARNING: Two files have same filename: %s\n", prevElem);
}
prevElem = filenameTableSorted[u];
}
free((void*)filenameTableSorted);
return 0;
return FIO_rust_checkFilenameCollisions(filenameTable, nbFiles);
}
char* UTIL_createFilenameFromOutDir(const char* path, const char* outDirName,
+117 -1
View File
@@ -10,7 +10,8 @@
//! implementation. In particular, the four legacy fields which C leaves
//! unspecified are not zero-filled here either.
use std::ffi::c_void;
use std::ffi::{c_void, CStr};
use std::io::{self, Write};
use std::mem::size_of;
use std::os::raw::{c_char, c_int, c_uint};
use std::ptr;
@@ -475,9 +476,91 @@ pub unsafe extern "C" fn FIO_determineHasStdinInput(
}
}
#[inline]
fn filename_path_separator() -> u8 {
if cfg!(windows) {
b'\\'
} else {
b'/'
}
}
/// Return the byte-level basename used for collision checks.
///
/// A null entry has no filename and is ignored by the exported checker. It
/// still maps to an empty key here so the key operation is safe to exercise
/// independently and never passes a null pointer to `CStr::from_ptr`.
unsafe fn filename_collision_key<'a>(filename: *const c_char) -> &'a [u8] {
if filename.is_null() {
return &[];
}
let bytes = unsafe { CStr::from_ptr(filename).to_bytes() };
match bytes
.iter()
.rposition(|&byte| byte == filename_path_separator())
{
Some(index) => &bytes[index + 1..],
None => bytes,
}
}
unsafe fn same_filename_collision_key(left: *const c_char, right: *const c_char) -> bool {
unsafe { filename_collision_key(left) == filename_collision_key(right) }
}
fn display_filename_collision_warning(filename: *const c_char) {
let enabled = unsafe { (*display_prefs()).displayLevel >= 2 };
if !enabled {
return;
}
let basename = unsafe { filename_collision_key(filename) };
let mut stderr = io::stderr().lock();
let _ = stderr.write_all(b"WARNING: Two files have same filename: ");
let _ = stderr.write_all(basename);
let _ = stderr.write_all(b"\n");
}
/// Rust implementation of the filename collision checker used by
/// `FIO_checkFilenameCollisions` in `programs/fileio.c`.
#[no_mangle]
pub unsafe extern "C" fn FIO_rust_checkFilenameCollisions(
filename_table: *const *const c_char,
nb_files: c_uint,
) -> c_int {
if filename_table.is_null() || nb_files == 0 {
return 0;
}
let mut filenames = Vec::<*const c_char>::new();
if filenames.try_reserve_exact(nb_files as usize).is_err() {
display(1, "Allocation error during filename collision checking \n");
return 1;
}
for index in 0..nb_files as usize {
let filename = unsafe { *filename_table.add(index) };
if !filename.is_null() {
filenames.push(filename);
}
}
filenames.sort_unstable_by(|left, right| unsafe {
filename_collision_key(*left).cmp(filename_collision_key(*right))
});
for pair in filenames.windows(2) {
if unsafe { same_filename_collision_key(pair[0], pair[1]) } {
display_filename_collision_warning(pair[0]);
}
}
0
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::mem::{align_of, offset_of, size_of};
#[test]
@@ -516,6 +599,39 @@ mod tests {
assert_eq!(size_of::<FIO_ctx_t>(), total_bytes + 2 * word);
}
#[test]
fn filename_collision_checker_handles_null_and_empty_inputs() {
let empty = CString::new("").unwrap();
let names = [ptr::null(), empty.as_ptr()];
unsafe {
assert_eq!(FIO_rust_checkFilenameCollisions(ptr::null(), 0), 0);
assert_eq!(FIO_rust_checkFilenameCollisions(names.as_ptr(), 2), 0);
assert!(same_filename_collision_key(ptr::null(), empty.as_ptr()));
}
}
#[test]
fn filename_collision_keys_compare_basename_bytes_without_utf8_conversion() {
let first = CString::new(b"left/\xffname".to_vec()).unwrap();
let duplicate = CString::new(b"right/\xffname".to_vec()).unwrap();
let different = CString::new(b"right/\xfename".to_vec()).unwrap();
let names = [first.as_ptr(), duplicate.as_ptr()];
unsafe {
assert_eq!(filename_collision_key(first.as_ptr()), b"\xffname");
assert!(same_filename_collision_key(
first.as_ptr(),
duplicate.as_ptr()
));
assert!(!same_filename_collision_key(
first.as_ptr(),
different.as_ptr()
));
assert_eq!(FIO_rust_checkFilenameCollisions(names.as_ptr(), 2), 0);
}
}
#[test]
fn preference_defaults_match_fileio_c() {
let prefs = unsafe { FIO_createPreferences() };