diff --git a/programs/fileio.c b/programs/fileio.c index 4b645a664..436f24862 100644 --- a/programs/fileio.c +++ b/programs/fileio.c @@ -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, diff --git a/rust/src/fileio_prefs.rs b/rust/src/fileio_prefs.rs index baf02c436..bb58cebe6 100644 --- a/rust/src/fileio_prefs.rs +++ b/rust/src/fileio_prefs.rs @@ -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::(), 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() };