refactor(cli): move directory source policy into Rust

The decompression source-open callback previously both tested for a named
directory and returned the generic open failure. That left a source-resource
rejection in the C orchestration path even though Rust already owns the
per-file ordering.

Add a C-owned directory probe to the projection. Rust invokes it before source
opening for named inputs and rejects a positive result; the C callback retains
the private filesystem helper and exact directory diagnostic. Stdin bypasses
the probe as before, and optional codec callbacks plus the remaining source
open, asynchronous, and cleanup ordering are unchanged.

Test Plan:
- rustfmt +nightly --check --edition 2021 rust/src/fileio_asyncio.rs (passed)
- capped GCC syntax-only check of programs/fileio.c with all optional-format
  macros enabled (passed)
- capped Clang syntax-only check of programs/fileio.c with all optional-format
  macros enabled (passed)
- git diff --cached --check (passed)
- workspace cargo +nightly fmt --check was not clean because of an unrelated
  rust/src/zstdmt_compress.rs formatting diff; that file was left untouched
- no cargo build/test, make, fuzzers, or upstream tests were run
- commit signing was disabled because the configured GPG prompt hung
This commit is contained in:
2026-07-20 17:16:24 +02:00
parent c89ac3d6cf
commit 62a9db5f65
2 changed files with 67 additions and 9 deletions
+25 -8
View File
@@ -1244,9 +1244,12 @@ typedef void (*FIO_rust_decompress_dst_stat_fn)(void* opaque, int dstFd,
typedef int (*FIO_rust_decompress_dst_close_fn)(void* opaque);
typedef void (*FIO_rust_decompress_dst_remove_fn)(void* opaque, const char* dstFileName);
typedef int (*FIO_rust_decompress_src_remove_fn)(void* opaque, const char* srcFileName);
typedef int (*FIO_rust_decompress_src_directory_fn)(void* opaque,
const char* srcFileName);
/* Rust owns per-file source/destination ordering; the callback context keeps
* dRess_t, FILE/pool handles, diagnostics, and the format dispatcher private. */
/* Rust owns per-file source/directory ordering and destination ordering; the
* callback context keeps dRess_t, FILE/pool handles, diagnostics, and the
* format dispatcher private. */
typedef struct {
void* opaque;
const char* dstFileName;
@@ -1271,6 +1274,7 @@ typedef struct {
FIO_rust_decompress_handler_fn utimeDestination;
FIO_rust_decompress_dst_remove_fn removeDestination;
FIO_rust_decompress_src_remove_fn removeSourceFile;
FIO_rust_decompress_src_directory_fn sourceIsDirectory;
} FIO_rust_decompress_file_projection_t;
typedef char FIO_rust_decompress_file_opaque_offset[
(offsetof(FIO_rust_decompress_file_projection_t, opaque) == 0) ? 1 : -1];
@@ -1299,11 +1303,16 @@ typedef char FIO_rust_decompress_file_callback_offset[
(offsetof(FIO_rust_decompress_file_projection_t, openSource)
== ((3 * sizeof(void*) + 5 * sizeof(int) + sizeof(void*) - 1)
/ sizeof(void*)) * sizeof(void*)) ? 1 : -1];
typedef char FIO_rust_decompress_file_source_directory_offset[
(offsetof(FIO_rust_decompress_file_projection_t, sourceIsDirectory)
== ((3 * sizeof(void*) + 5 * sizeof(int) + sizeof(void*) - 1)
/ sizeof(void*)) * sizeof(void*)
+ 15 * sizeof(void*)) ? 1 : -1];
typedef char FIO_rust_decompress_file_projection_size[
(sizeof(FIO_rust_decompress_file_projection_t)
== ((3 * sizeof(void*) + 5 * sizeof(int) + sizeof(void*) - 1)
/ sizeof(void*)) * sizeof(void*)
+ 15 * sizeof(FIO_rust_decompress_src_open_fn)) ? 1 : -1];
+ 16 * sizeof(FIO_rust_decompress_src_open_fn)) ? 1 : -1];
int FIO_rust_decompressFilename(
const FIO_rust_decompress_file_projection_t* projection);
@@ -4249,11 +4258,6 @@ static int FIO_rust_decompressSourceOpen(void* opaque, const char* srcFileName,
FIO_rust_decompress_file_context_t* const context =
(FIO_rust_decompress_file_context_t*)opaque;
if (UTIL_isDirectory(srcFileName)) {
DISPLAYLEVEL(1, "zstd: %s is a directory -- ignored \n", srcFileName);
return 1;
}
context->srcFile = FIO_openSrcFile(context->prefs, srcFileName, &context->srcFileStat);
if (context->srcFile == NULL)
return 1;
@@ -4267,6 +4271,18 @@ static int FIO_rust_decompressSourceOpen(void* opaque, const char* srcFileName,
return FIO_RUST_DECOMPRESS_SRC_OPEN_OK;
}
static int FIO_rust_decompressSourceIsDirectory(void* opaque, const char* srcFileName)
{
/* Rust owns the rejection decision; keep the probe and exact diagnostic
* here because they depend on the CLI's private filesystem helpers. */
(void)opaque;
if (UTIL_isDirectory(srcFileName)) {
DISPLAYLEVEL(1, "zstd: %s is a directory -- ignored \n", srcFileName);
return 1;
}
return 0;
}
static void FIO_rust_decompressSourceSetAsync(void* opaque, int asyncMode)
{
FIO_rust_decompress_file_context_t* const context =
@@ -4433,6 +4449,7 @@ static int FIO_decompressFile(FIO_ctx_t* const fCtx, FIO_prefs_t* const prefs,
projection.utimeDestination = FIO_rust_decompressDestinationUtime;
projection.removeDestination = FIO_rust_decompressDestinationRemove;
projection.removeSourceFile = FIO_rust_decompressSourceRemove;
projection.sourceIsDirectory = FIO_rust_decompressSourceIsDirectory;
return FIO_rust_decompressFilename(&projection);
}
+42 -1
View File
@@ -671,6 +671,8 @@ const FIO_RUST_DECOMPRESS_SRC_UNKNOWN_SIZE: u64 = u64::MAX;
pub type FIO_rust_decompress_src_open_fn =
unsafe extern "C" fn(*mut c_void, *const c_char, *mut u64, *mut c_int) -> c_int;
pub type FIO_rust_decompress_src_directory_fn =
unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int;
pub type FIO_rust_decompress_src_async_fn = unsafe extern "C" fn(*mut c_void, c_int);
pub type FIO_rust_decompress_src_attach_fn = unsafe extern "C" fn(*mut c_void);
pub type FIO_rust_decompress_src_close_fn = unsafe extern "C" fn(*mut c_void) -> c_int;
@@ -713,6 +715,7 @@ pub struct FIO_rust_decompress_file_projection_t {
pub utime_destination: Option<FIO_rust_decompress_handler_fn>,
pub remove_destination: Option<FIO_rust_decompress_dst_remove_fn>,
pub remove_source_file: Option<FIO_rust_decompress_src_remove_fn>,
pub source_is_directory: Option<FIO_rust_decompress_src_directory_fn>,
}
const FIO_RUST_DECOMPRESS_FILE_CALLBACK_OFFSET: usize =
@@ -812,9 +815,13 @@ const _: () = {
== FIO_RUST_DECOMPRESS_FILE_CALLBACK_OFFSET + 14 * size_of::<usize>()
);
assert!(
size_of::<FIO_rust_decompress_file_projection_t>()
std::mem::offset_of!(FIO_rust_decompress_file_projection_t, source_is_directory)
== FIO_RUST_DECOMPRESS_FILE_CALLBACK_OFFSET + 15 * size_of::<usize>()
);
assert!(
size_of::<FIO_rust_decompress_file_projection_t>()
== FIO_RUST_DECOMPRESS_FILE_CALLBACK_OFFSET + 16 * size_of::<usize>()
);
};
pub const FIO_RUST_ZSTD_OK: c_int = 0;
@@ -3073,6 +3080,15 @@ pub unsafe extern "C" fn FIO_rust_decompressFilename(
assert!(!projection.dst_file_name.is_null());
assert!(!projection.src_file_name.is_null());
if projection.source_is_stdin == 0 {
let source_is_directory = projection
.source_is_directory
.expect("source directory callback is required for named files");
if unsafe { source_is_directory(projection.opaque, projection.src_file_name) } != 0 {
return 1;
}
}
let open_source = projection
.open_source
.expect("source open callback is required");
@@ -5869,6 +5885,8 @@ mod tests {
destination_fds: Vec<c_int>,
source_file_size: u64,
source_is_regular: c_int,
source_is_directory: c_int,
source_directory_checks: usize,
source_open_status: c_int,
source_close_status: c_int,
destination_open_status: c_int,
@@ -5892,6 +5910,15 @@ mod tests {
state.source_open_status
}
unsafe extern "C" fn decompress_policy_source_is_directory(
opaque: *mut c_void,
_src_file_name: *const c_char,
) -> c_int {
let state = unsafe { &mut *opaque.cast::<DecompressPolicyState>() };
state.source_directory_checks += 1;
state.source_is_directory
}
unsafe extern "C" fn decompress_policy_async(opaque: *mut c_void, async_mode: c_int) {
let state = unsafe { &mut *opaque.cast::<DecompressPolicyState>() };
state.events.push(DECOMPRESS_POLICY_ASYNC);
@@ -6023,9 +6050,23 @@ mod tests {
utime_destination: Some(decompress_policy_utime_destination),
remove_destination: Some(decompress_policy_remove_destination),
remove_source_file: Some(decompress_policy_remove_source),
source_is_directory: Some(decompress_policy_source_is_directory),
}
}
#[test]
fn decompress_file_policy_rejects_directory_before_opening_source() {
let mut state = DecompressPolicyState {
source_is_directory: 1,
..DecompressPolicyState::default()
};
let projection = decompress_policy_projection(&mut state, 0, 0, 0, 0, 0);
assert_eq!(unsafe { FIO_rust_decompressFilename(&projection) }, 1);
assert_eq!(state.source_directory_checks, 1);
assert!(state.events.is_empty());
}
#[test]
fn decompress_file_policy_orders_cleanup_and_successful_source_removal() {
let mut state = DecompressPolicyState {