feat(cli): move dictionary release dispatch into Rust

Route FIO_freeDict through the Rust filesystem backend for malloc-backed and
mapped dictionaries. Keep enum validation in C, pair Windows views with their
file handles, support the non-POSIX malloc fallback, and clear every ownership
field so repeated cleanup remains harmless.

Test Plan:
- cargo test --manifest-path rust/cli/Cargo.toml --lib fileio_backend -- --test-threads=1 (25 passed)
- cargo clippy --manifest-path rust/cli/Cargo.toml --lib -- -D warnings
- rustfmt +nightly --edition 2021 rust/src/fileio_backend.rs --check
- make -C programs -j2 zstd
- git diff --check
This commit is contained in:
2026-07-18 17:45:19 +02:00
parent e557549ff4
commit 643cb45d89
2 changed files with 116 additions and 24 deletions
+96 -1
View File
@@ -454,6 +454,13 @@ pub unsafe extern "C" fn FIO_rust_munmapDict(
}
}
#[cfg(not(any(unix, windows)))]
{
if !mapping.is_null() {
unsafe { libc::free(mapping) };
}
}
unsafe {
*buffer = ptr::null_mut();
*buffer_size = 0;
@@ -463,6 +470,47 @@ pub unsafe extern "C" fn FIO_rust_munmapDict(
}
}
const FIO_MALLOC_DICT: c_int = 0;
const FIO_MMAP_DICT: c_int = 1;
/// Releases either kind of dictionary buffer and clears its C-visible fields.
///
/// C retains the enum validation and calls this leaf only for the two valid
/// `FIO_dictBufferType_t` values. Mapped buffers go through the existing
/// platform-aware release leaf so Windows views and file handles remain paired
/// correctly, while malloc-backed buffers use the matching libc allocator.
#[no_mangle]
pub unsafe extern "C" fn FIO_rust_freeDict(
dict_buffer_type: c_int,
buffer: *mut *mut c_void,
buffer_size: *mut usize,
dict_handle: *mut *mut c_void,
) {
match dict_buffer_type {
FIO_MALLOC_DICT => {
if !buffer.is_null() {
let allocation = unsafe { *buffer };
if !allocation.is_null() {
unsafe { libc::free(allocation) };
}
}
unsafe {
if !buffer.is_null() {
*buffer = ptr::null_mut();
}
if !buffer_size.is_null() {
*buffer_size = 0;
}
if !dict_handle.is_null() {
*dict_handle = ptr::null_mut();
}
}
}
FIO_MMAP_DICT => unsafe { FIO_rust_munmapDict(buffer, buffer_size, dict_handle) },
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -664,12 +712,59 @@ mod tests {
let mapped = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), mapped_size) };
assert_eq!(mapped, payload);
unsafe { FIO_rust_munmapDict(&mut buffer, &mut mapped_size, ptr::null_mut()) };
let mut dict_handle = 1usize as *mut c_void;
unsafe {
FIO_rust_freeDict(
FIO_MMAP_DICT,
&mut buffer,
&mut mapped_size,
&mut dict_handle,
)
};
assert!(buffer.is_null());
assert_eq!(mapped_size, 0);
assert!(dict_handle.is_null());
// The release leaf is idempotent after the dispatch has cleared the
// ownership fields, so cleanup cannot double-release the mapping.
unsafe { FIO_rust_munmapDict(&mut buffer, &mut mapped_size, &mut dict_handle) };
fs::remove_file(path).expect("remove temporary file");
}
#[test]
fn frees_malloc_dictionary_and_clears_ownership() {
let allocation = unsafe { libc::malloc(16) };
assert!(!allocation.is_null());
let mut buffer = allocation;
let mut buffer_size = 16;
let mut dict_handle = 1usize as *mut c_void;
unsafe {
FIO_rust_freeDict(
FIO_MALLOC_DICT,
&mut buffer,
&mut buffer_size,
&mut dict_handle,
)
};
assert!(buffer.is_null());
assert_eq!(buffer_size, 0);
assert!(dict_handle.is_null());
// A second release sees only cleared fields and must not double-free.
unsafe {
FIO_rust_freeDict(
FIO_MALLOC_DICT,
&mut buffer,
&mut buffer_size,
&mut dict_handle,
)
};
assert!(buffer.is_null());
assert_eq!(buffer_size, 0);
assert!(dict_handle.is_null());
}
#[cfg(unix)]
#[test]
fn maps_empty_dictionary_with_owned_zero_size_sentinel() {