diff --git a/programs/fileio.c b/programs/fileio.c index 69505379d..df7babb01 100644 --- a/programs/fileio.c +++ b/programs/fileio.c @@ -350,6 +350,7 @@ const char* FIO_rust_determineDstName(const char* srcFileName, const char* outDi int FIO_rust_adjustMemLimitForPatchFromMode(FIO_prefs_t* prefs, unsigned long long dictSize, unsigned long long maxSrcFileSize); +int FIO_rust_getDictFileStat(const char* fileName, stat_t* statBuf); int FIO_rust_setDictBufferMalloc(const char* fileName, unsigned long long expectedFileSize, size_t maxSize, @@ -543,16 +544,26 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs, /* FIO_getDictFileStat() : */ static void FIO_getDictFileStat(const char* fileName, stat_t* dictFileStat) { + enum { + FIO_DICT_STAT_SUCCESS = 0, + FIO_DICT_STAT_FAILED = 1, + FIO_DICT_STAT_NON_REGULAR = 2, + }; + int status; + assert(dictFileStat != NULL); if (fileName == NULL) return; - if (!UTIL_stat(fileName, dictFileStat)) { + status = FIO_rust_getDictFileStat(fileName, dictFileStat); + if (status == FIO_DICT_STAT_FAILED) { EXM_THROW(31, "Stat failed on dictionary file %s: %s", fileName, strerror(errno)); } - if (!UTIL_isRegularFileStat(dictFileStat)) { + if (status == FIO_DICT_STAT_NON_REGULAR) { EXM_THROW(32, "Dictionary %s must be a regular file.", fileName); } + + assert(status == FIO_DICT_STAT_SUCCESS); } /* FIO_setDictBufferMalloc() : diff --git a/rust/src/fileio_backend.rs b/rust/src/fileio_backend.rs index b28e06a38..ada4ae1f6 100644 --- a/rust/src/fileio_backend.rs +++ b/rust/src/fileio_backend.rs @@ -26,6 +26,15 @@ const FIO_DICT_LOAD_TOO_LARGE: c_int = 2; const FIO_DICT_LOAD_ALLOCATION_FAILED: c_int = 3; const FIO_DICT_LOAD_READ_FAILED: c_int = 4; +const FIO_DICT_STAT_SUCCESS: c_int = 0; +const FIO_DICT_STAT_FAILED: c_int = 1; +const FIO_DICT_STAT_NON_REGULAR: c_int = 2; + +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; +} + fn path_from_c(path: *const c_char) -> Option { if path.is_null() { return None; @@ -42,6 +51,29 @@ fn path_from_c(path: *const c_char) -> Option { } } +/// Stats a dictionary path into the C-owned `stat_t` buffer. +/// +/// The C wrapper keeps the policy and diagnostics. This leaf only reports +/// whether the existing stat ABI failed, found a non-regular path, or +/// succeeded, so `stat_t` layout, errno, and symlink handling stay centralized +/// in `UTIL_stat` and `UTIL_isRegularFileStat`. +#[no_mangle] +pub unsafe extern "C" fn FIO_rust_getDictFileStat( + file_name: *const c_char, + stat_buf: *mut libc::stat, +) -> c_int { + if file_name.is_null() || stat_buf.is_null() { + return FIO_DICT_STAT_FAILED; + } + if unsafe { UTIL_stat(file_name, stat_buf) } == 0 { + return FIO_DICT_STAT_FAILED; + } + if unsafe { UTIL_isRegularFileStat(stat_buf) } == 0 { + return FIO_DICT_STAT_NON_REGULAR; + } + FIO_DICT_STAT_SUCCESS +} + /// Removes a regular file and returns a status consumed by the C adapter. /// /// The metadata query follows symlinks, matching the existing `stat()`-based @@ -139,6 +171,7 @@ mod tests { use super::*; use std::ffi::CString; use std::fs; + use std::mem::MaybeUninit; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -160,6 +193,13 @@ mod tests { CString::new(path.to_string_lossy().as_bytes()).expect("temporary path contains NUL") } + fn get_dict_file_stat(path: Option<&Path>) -> c_int { + let path = path.map(c_path); + let file_name = path.as_ref().map_or(ptr::null(), |path| path.as_ptr()); + let mut stat_buf = MaybeUninit::::uninit(); + unsafe { FIO_rust_getDictFileStat(file_name, stat_buf.as_mut_ptr()) } + } + fn load_dict(path: &Path, expected_size: u64, max_size: usize) -> (c_int, *mut c_void, usize) { let path = c_path(path); let mut buffer = ptr::null_mut(); @@ -275,6 +315,54 @@ mod tests { fs::remove_file(&path).expect("remove temporary file"); } + #[test] + fn accepts_regular_dictionary_file() { + let path = temp_path("regular-dictionary-stat"); + fs::write(&path, b"dictionary").expect("create temporary file"); + + assert_eq!(get_dict_file_stat(Some(&path)), FIO_DICT_STAT_SUCCESS); + + fs::remove_file(&path).expect("remove temporary file"); + } + + #[test] + fn reports_missing_dictionary_path() { + let path = temp_path("missing-dictionary-stat"); + + assert_eq!(get_dict_file_stat(Some(&path)), FIO_DICT_STAT_FAILED); + } + + #[test] + fn reports_directory_as_non_regular() { + let path = temp_path("directory-dictionary-stat"); + fs::create_dir(&path).expect("create temporary directory"); + + assert_eq!(get_dict_file_stat(Some(&path)), FIO_DICT_STAT_NON_REGULAR); + + fs::remove_dir(&path).expect("remove temporary directory"); + } + + #[test] + fn reports_null_filename_as_stat_failure() { + assert_eq!(get_dict_file_stat(None), FIO_DICT_STAT_FAILED); + } + + #[cfg(unix)] + #[test] + fn follows_symlink_to_regular_dictionary_file() { + use std::os::unix::fs::symlink; + + let target = temp_path("symlink-target"); + let link = temp_path("symlink-dictionary"); + fs::write(&target, b"dictionary").expect("create temporary file"); + symlink(&target, &link).expect("create symbolic link"); + + assert_eq!(get_dict_file_stat(Some(&link)), FIO_DICT_STAT_SUCCESS); + + fs::remove_file(&link).expect("remove symbolic link"); + fs::remove_file(&target).expect("remove temporary file"); + } + #[test] fn removes_regular_file() { let path = temp_path("regular");