From 42481fb36676c1271314270b7a782fe89572f4d8 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 18 Jul 2026 04:14:40 +0200 Subject: [PATCH] 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 --- programs/fileio.c | 99 +------------------ rust/src/fileio_prefs.rs | 200 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 96 deletions(-) diff --git a/programs/fileio.c b/programs/fileio.c index bd08fbf35..cf46b02ab 100644 --- a/programs/fileio.c +++ b/programs/fileio.c @@ -312,6 +312,8 @@ unsigned long long FIO_rust_getLargestFileSize(const char** inFileNames, unsigne void FIO_rust_setInBuffer(ZSTD_inBuffer* output, const void* buf, size_t s, size_t pos); void FIO_rust_setOutBuffer(ZSTD_outBuffer* output, void* buf, size_t s, size_t pos); const char* FIO_rust_determineCompressedName(const char* srcFileName, const char* outDirName, const char* suffix); +const char* FIO_rust_determineDstName(const char* srcFileName, const char* outDirName, + const char* const* suffixList, const char* suffixListStr); #ifdef ZSTD_LZ4COMPRESS int FIO_rust_LZ4_GetBlockSize_FromBlockId(int id); #endif @@ -690,20 +692,6 @@ int FIO_checkFilenameCollisions(const char** filenameTable, unsigned nbFiles) { return FIO_rust_checkFilenameCollisions(filenameTable, nbFiles); } -char* UTIL_createFilenameFromOutDir(const char* path, const char* outDirName, - size_t suffixLen); - -/* FIO_createFilename_fromOutDir() : - * Takes a source file name and specified output directory, and - * allocates memory for and returns a pointer to final path. - * This function never returns an error (it may abort() in case of pb) - */ -static char* -FIO_createFilename_fromOutDir(const char* path, const char* outDirName, const size_t suffixLen) -{ - return UTIL_createFilenameFromOutDir(path, outDirName, suffixLen); -} - /* FIO_highbit64() : * gives position of highest bit. * note : only works for v > 0 ! @@ -2705,88 +2693,7 @@ static const char *suffixListStr = static const char* FIO_determineDstName(const char* srcFileName, const char* outDirName) { - static size_t dfnbCapacity = 0; - static char* dstFileNameBuffer = NULL; /* using static allocation : this function cannot be multi-threaded */ - size_t dstFileNameEndPos; - char* outDirFilename = NULL; - const char* dstSuffix = ""; - size_t dstSuffixLen = 0; - - size_t sfnSize = strlen(srcFileName); - - size_t srcSuffixLen; - const char* const srcSuffix = strrchr(srcFileName, '.'); - - if(!strcmp(srcFileName, stdinmark)) { - return stdoutmark; - } - - if (srcSuffix == NULL) { - DISPLAYLEVEL(1, - "zstd: %s: unknown suffix (%s expected). " - "Can't derive the output file name. " - "Specify it with -o dstFileName. Ignoring.\n", - srcFileName, suffixListStr); - return NULL; - } - srcSuffixLen = strlen(srcSuffix); - - { - const char** matchedSuffixPtr; - for (matchedSuffixPtr = suffixList; *matchedSuffixPtr != NULL; matchedSuffixPtr++) { - if (!strcmp(*matchedSuffixPtr, srcSuffix)) { - break; - } - } - - /* check suffix is authorized */ - if (sfnSize <= srcSuffixLen || *matchedSuffixPtr == NULL) { - DISPLAYLEVEL(1, - "zstd: %s: unknown suffix (%s expected). " - "Can't derive the output file name. " - "Specify it with -o dstFileName. Ignoring.\n", - srcFileName, suffixListStr); - return NULL; - } - - if ((*matchedSuffixPtr)[1] == 't') { - dstSuffix = ".tar"; - dstSuffixLen = strlen(dstSuffix); - } - } - - if (outDirName) { - outDirFilename = FIO_createFilename_fromOutDir(srcFileName, outDirName, 0); - sfnSize = strlen(outDirFilename); - assert(outDirFilename != NULL); - } - - if (dfnbCapacity+srcSuffixLen <= sfnSize+1+dstSuffixLen) { - /* allocate enough space to write dstFilename into it */ - free(dstFileNameBuffer); - dfnbCapacity = sfnSize + 20; - dstFileNameBuffer = (char*)malloc(dfnbCapacity); - if (dstFileNameBuffer==NULL) - EXM_THROW(74, "%s : not enough memory for dstFileName", - strerror(errno)); - } - - /* return dst name == src name truncated from suffix */ - assert(dstFileNameBuffer != NULL); - dstFileNameEndPos = sfnSize - srcSuffixLen; - if (outDirFilename) { - memcpy(dstFileNameBuffer, outDirFilename, dstFileNameEndPos); - free(outDirFilename); - } else { - memcpy(dstFileNameBuffer, srcFileName, dstFileNameEndPos); - } - - /* The short tar extensions tzst, tgz, txz and tlz4 files should have "tar" - * extension on decompression. Also writes terminating null. */ - strcpy(dstFileNameBuffer + dstFileNameEndPos, dstSuffix); - return dstFileNameBuffer; - - /* note : dstFileNameBuffer memory is not going to be free */ + return FIO_rust_determineDstName(srcFileName, outDirName, suffixList, suffixListStr); } int diff --git a/rust/src/fileio_prefs.rs b/rust/src/fileio_prefs.rs index dadea2ee7..7fc22c975 100644 --- a/rust/src/fileio_prefs.rs +++ b/rust/src/fileio_prefs.rs @@ -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::()); + let new_capacity = source_len.wrapping_add(20); + let new_buffer = libc::malloc(new_capacity).cast::(); + 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::(), + destination_end, + ); + ptr::copy_nonoverlapping( + dst_suffix.as_ptr(), + destination.add(destination_end).cast::(), + dst_suffix.len(), + ); + if !out_dir_filename.is_null() { + libc::free(out_dir_filename.cast::()); + } + 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::();