feat(cli): move dictionary stat leaf to Rust

Move dictionary-path statting behind the existing Rust CLI file-I/O backend while keeping FIO_getDictFileStat as the C policy adapter. The adapter still owns the NULL-path early return, errno-based EXM_THROW(31) diagnostic, EXM_THROW(32) regular-file policy, and all dictionary orchestration.

FIO_rust_getDictFileStat crosses only the existing path and stat_t boundary, delegates metadata and regular-file classification to UTIL_stat and UTIL_isRegularFileStat, and returns 0/1/2 for success, stat failure, or non-regular input. Reusing those ABIs preserves the C stat_t representation, errno behavior, and stat's symlink-following semantics. Focused backend tests cover regular, missing, directory, NULL, and symlink paths.

Test Plan:
- cargo test --manifest-path rust/cli/Cargo.toml --no-default-features --features cli,compression,decompression --lib fileio_backend (15 passed)
- cargo clippy --no-default-features --features compression, including --benches and --tests (passed)
- CLI cargo clippy with cli,compression,decompression,benchmark, including --benches and --tests (passed)
- cargo +nightly fmt checks for the owned Rust module and CLI crate (passed); root-wide check remains limited by unrelated dict_builder_zdict.rs edits
- Existing CLI dictionary scenarios for missing and directory paths retained status 31/32 and exact diagnostics
- git diff --check and git diff --cached --check (passed)
- zstd build variants were attempted; full links remain blocked by unrelated active dict-builder/zstd-cli/archive edits after the owned C file compiled
This commit is contained in:
2026-07-18 15:52:15 +02:00
parent 821f01cf44
commit abcc066622
2 changed files with 101 additions and 2 deletions
+13 -2
View File
@@ -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() :
+88
View File
@@ -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<PathBuf> {
if path.is_null() {
return None;
@@ -42,6 +51,29 @@ fn path_from_c(path: *const c_char) -> Option<PathBuf> {
}
}
/// 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::<libc::stat>::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");