feat(cli): move decompressed filename policy to Rust

Port destination-name derivation to the Rust file-I/O preference module. Preserve build-configured suffix matching, tar-name conversion, stdin/stdout sentinels, output-directory handling, diagnostics, static-buffer reuse, and allocation behavior through the existing C adapter.

Test Plan:\n- cargo test --manifest-path rust/cli/Cargo.toml --no-default-features --features cli,compression,decompression,benchmark\n- cargo test --manifest-path rust/cli/Cargo.toml --no-default-features --features helpers\n- cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets --no-default-features --features cli,compression,decompression,benchmark\n- cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets --no-default-features --features helpers\n- make -B -C programs -j2 zstd zstd-small zstd-frugal\n- make -C tests -j2 test-cli-tests
This commit is contained in:
2026-07-18 04:14:40 +02:00
parent e80fbe4b06
commit 42481fb366
2 changed files with 203 additions and 96 deletions
+200
View File
@@ -23,6 +23,8 @@ static STDOUT_MARK: &[u8] = b"/*stdout*\\\0";
static mut COMPRESSED_NAME_CAPACITY: usize = 0;
static mut COMPRESSED_NAME_BUFFER: *mut c_char = ptr::null_mut();
static mut DESTINATION_NAME_CAPACITY: usize = 0;
static mut DESTINATION_NAME_BUFFER: *mut c_char = ptr::null_mut();
#[repr(C)]
pub struct FIO_inBuffer {
@@ -649,6 +651,131 @@ pub unsafe extern "C" fn FIO_rust_determineCompressedName(
}
}
fn display_unknown_suffix(source: &[u8], suffix_list: &[u8]) {
let enabled = unsafe { (*display_prefs()).displayLevel >= 1 };
if !enabled {
return;
}
let mut stderr = io::stderr().lock();
let _ = stderr.write_all(b"zstd: ");
let _ = stderr.write_all(source);
let _ = stderr.write_all(b": unknown suffix (");
let _ = stderr.write_all(suffix_list);
let _ = stderr.write_all(
b" expected). Can't derive the output file name. Specify it with -o dstFileName. Ignoring.\n",
);
}
/// Build the decompressed destination name used by the multi-file CLI path.
///
/// The suffix table and its display string come from C because both are
/// selected by the C build configuration. As with the original helper, the
/// returned pointer refers to a static buffer and is overwritten by the next
/// successful call.
#[no_mangle]
pub unsafe extern "C" fn FIO_rust_determineDstName(
src_file_name: *const c_char,
out_dir_name: *const c_char,
suffix_list: *const *const c_char,
suffix_list_str: *const c_char,
) -> *const c_char {
let source = unsafe { CStr::from_ptr(src_file_name).to_bytes() };
let suffix_display = unsafe { CStr::from_ptr(suffix_list_str).to_bytes() };
if source == b"/*stdin*\\" {
return STDOUT_MARK.as_ptr().cast();
}
let Some(src_suffix_start) = source.iter().rposition(|&byte| byte == b'.') else {
display_unknown_suffix(source, suffix_display);
return ptr::null();
};
let src_suffix = &source[src_suffix_start..];
let src_suffix_len = src_suffix.len();
let mut matched_suffix = ptr::null();
let mut suffix_ptr = suffix_list;
unsafe {
while !suffix_ptr.is_null() {
let candidate = *suffix_ptr;
if candidate.is_null() {
break;
}
if CStr::from_ptr(candidate).to_bytes() == src_suffix {
matched_suffix = candidate;
break;
}
suffix_ptr = suffix_ptr.add(1);
}
}
if source.len() <= src_suffix_len || matched_suffix.is_null() {
display_unknown_suffix(source, suffix_display);
return ptr::null();
}
let matched_suffix_bytes = unsafe { CStr::from_ptr(matched_suffix).to_bytes() };
let dst_suffix: &[u8] = if matched_suffix_bytes.get(1) == Some(&b't') {
b".tar\0"
} else {
b"\0"
};
let dst_suffix_len = dst_suffix.len() - 1;
let mut out_dir_filename = ptr::null_mut();
let source_for_destination = if out_dir_name.is_null() {
source
} else {
out_dir_filename =
unsafe { crate::util::UTIL_createFilenameFromOutDir(src_file_name, out_dir_name, 0) };
assert!(!out_dir_filename.is_null());
unsafe { CStr::from_ptr(out_dir_filename).to_bytes() }
};
let source_len = source_for_destination.len();
unsafe {
let capacity = ptr::addr_of_mut!(DESTINATION_NAME_CAPACITY);
let buffer = ptr::addr_of_mut!(DESTINATION_NAME_BUFFER);
if capacity.read().wrapping_add(src_suffix_len)
<= source_len.wrapping_add(1).wrapping_add(dst_suffix_len)
{
libc::free(buffer.read().cast::<c_void>());
let new_capacity = source_len.wrapping_add(20);
let new_buffer = libc::malloc(new_capacity).cast::<c_char>();
if new_buffer.is_null() {
throw(
74,
&format!(
"{} : not enough memory for dstFileName",
std::io::Error::last_os_error()
),
);
}
capacity.write(new_capacity);
buffer.write(new_buffer);
}
let destination = buffer.read();
assert!(!destination.is_null());
let destination_end = source_len - src_suffix_len;
ptr::copy_nonoverlapping(
source_for_destination.as_ptr(),
destination.cast::<u8>(),
destination_end,
);
ptr::copy_nonoverlapping(
dst_suffix.as_ptr(),
destination.add(destination_end).cast::<u8>(),
dst_suffix.len(),
);
if !out_dir_filename.is_null() {
libc::free(out_dir_filename.cast::<c_void>());
}
destination.cast()
}
}
#[inline]
fn filename_path_separator() -> u8 {
if cfg!(windows) {
@@ -877,6 +1004,79 @@ mod tests {
}
}
#[test]
fn destination_filename_shim_preserves_suffixes_tar_names_and_sentinels() {
let zst = CString::new(".zst").unwrap();
let tzst = CString::new(".tzst").unwrap();
let gz = CString::new(".gz").unwrap();
let tgz = CString::new(".tgz").unwrap();
let suffixes = [
zst.as_ptr(),
tzst.as_ptr(),
gz.as_ptr(),
tgz.as_ptr(),
ptr::null(),
];
let suffixes_display = CString::new(".zst/.tzst/.gz/.tgz").unwrap();
let source = CString::new("input/nested/file.zst").unwrap();
let tar_source = CString::new("input/nested/archive.tzst").unwrap();
let tar_gzip_source = CString::new("archive.tgz").unwrap();
let output_dir = CString::new("out").unwrap();
let stdin = CString::new("/*stdin*\\").unwrap();
unsafe {
let name = FIO_rust_determineDstName(
source.as_ptr(),
ptr::null(),
suffixes.as_ptr(),
suffixes_display.as_ptr(),
);
assert_eq!(CStr::from_ptr(name).to_bytes(), b"input/nested/file");
let name = FIO_rust_determineDstName(
tar_source.as_ptr(),
output_dir.as_ptr(),
suffixes.as_ptr(),
suffixes_display.as_ptr(),
);
assert_eq!(CStr::from_ptr(name).to_bytes(), b"out/archive.tar");
let name = FIO_rust_determineDstName(
tar_gzip_source.as_ptr(),
ptr::null(),
suffixes.as_ptr(),
suffixes_display.as_ptr(),
);
assert_eq!(CStr::from_ptr(name).to_bytes(), b"archive.tar");
let name = FIO_rust_determineDstName(
stdin.as_ptr(),
output_dir.as_ptr(),
suffixes.as_ptr(),
suffixes_display.as_ptr(),
);
assert_eq!(CStr::from_ptr(name).to_bytes(), b"/*stdout*\\");
}
}
#[test]
fn destination_filename_shim_rejects_unknown_suffix() {
let zst = CString::new(".zst").unwrap();
let suffixes = [zst.as_ptr(), ptr::null()];
let suffixes_display = CString::new(".zst").unwrap();
let source = CString::new("input/file.zip").unwrap();
unsafe {
assert!(FIO_rust_determineDstName(
source.as_ptr(),
ptr::null(),
suffixes.as_ptr(),
suffixes_display.as_ptr(),
)
.is_null());
}
}
#[test]
fn c_layouts_match_the_headers() {
let word = size_of::<usize>();