From be7e656d8e819261ffeeb2731d84658d8edf7a05 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 18 Jul 2026 22:41:39 +0200 Subject: [PATCH] feat(cli): move compression format dispatch into Rust `FIO_compressFilename_internal()` still selected the zstd, gzip, xz/lzma, and lz4 paths in a C-owned format switch and updated aggregate byte accounting around those callbacks. That left the mixed-format CLI dispatch loop outside the Rust file-I/O orchestration even though the pools and decompression path were already projected there. Add a callback table for the codec leaves and move format selection, optional codec status handling, post-success accounting, and progress-hook ordering to Rust. The existing C codec implementations, diagnostics, elapsed-time formatting, and resource ownership remain unchanged; unsupported optional formats return explicit statuses so the C wrapper preserves its original EXM_THROW messages. Test Plan: - `cargo test --manifest-path rust/Cargo.toml --all-targets -- --test-threads=1` -- 473 passed. - Native `test-cli-tests` -- all 41 passed. - Native `test-zstd` -- passed, including async zstd/gzip/xz/lzma/lz4 round trips. - `make -B -C programs -j2 zstd` and `make -B -C tests/fuzz -j2 all` -- passed. --- programs/fileio.c | 224 ++++++++++++----- rust/src/fileio_asyncio.rs | 490 +++++++++++++++++++++++++++++++++++++ 2 files changed, 657 insertions(+), 57 deletions(-) diff --git a/programs/fileio.c b/programs/fileio.c index ad9ac5260..3a6d0fcd6 100644 --- a/programs/fileio.c +++ b/programs/fileio.c @@ -975,6 +975,49 @@ typedef struct { ReadPoolCtx_t *readCtx; } cRess_t; +enum { + FIO_RUST_COMPRESS_OK = 0, + FIO_RUST_COMPRESS_GZIP_UNSUPPORTED = 1, + FIO_RUST_COMPRESS_LZMA_UNSUPPORTED = 2, + FIO_RUST_COMPRESS_LZ4_UNSUPPORTED = 3, + FIO_RUST_COMPRESS_ZSTD_UNSUPPORTED = 4 +}; + +typedef unsigned long long (*FIO_rust_compress_zstd_fn)( + void* fCtx, void* prefs, void* ress, + const char* srcFileName, U64 srcFileSize, + int compressionLevel, U64* readsize); +typedef unsigned long long (*FIO_rust_compress_gzip_fn)( + void* ress, const char* srcFileName, U64 srcFileSize, + int compressionLevel, U64* readsize); +typedef unsigned long long (*FIO_rust_compress_lzma_fn)( + void* ress, const char* srcFileName, U64 srcFileSize, + int compressionLevel, U64* readsize, int plainLzma); +typedef unsigned long long (*FIO_rust_compress_lz4_fn)( + void* ress, const char* srcFileName, U64 srcFileSize, + int compressionLevel, int checksumFlag, U64* readsize); +typedef void (*FIO_rust_compress_input_display_fn)( + void* opaque, const char* srcFileName, U64 fileSize); +typedef void (*FIO_rust_compress_status_display_fn)( + void* opaque, void* fCtx, const char* dstFileName, + const char* srcFileName, U64 readsize, U64 compressedfilesize); + +typedef struct { + void* opaque; + FIO_rust_compress_zstd_fn compress_zstd; + FIO_rust_compress_gzip_fn compress_gzip; + FIO_rust_compress_lzma_fn compress_lzma; + FIO_rust_compress_lz4_fn compress_lz4; + FIO_rust_compress_input_display_fn display_input; + FIO_rust_compress_status_display_fn display_status; +} FIO_rust_compress_callbacks_t; + +int FIO_rust_compressFilenameInternal( + void* fCtx, FIO_prefs_t* prefs, void* ress, + const char* dstFileName, const char* srcFileName, + U64 fileSize, int compressionLevel, + const FIO_rust_compress_callbacks_t* callbacks); + static void FIO_adjustParamsForPatchFromMode(FIO_prefs_t* const prefs, ZSTD_compressionParameters* comprParams, unsigned long long const dictSize, @@ -1632,71 +1675,79 @@ FIO_compressZstdFrame(FIO_ctx_t* const fCtx, return compressedfilesize; } -/*! FIO_compressFilename_internal() : - * same as FIO_compressFilename_extRess(), with `ress.desFile` already opened. - * @return : 0 : compression completed correctly, - * 1 : missing or pb opening srcFileName - */ -static int -FIO_compressFilename_internal(FIO_ctx_t* const fCtx, - FIO_prefs_t* const prefs, - cRess_t ress, - const char* dstFileName, const char* srcFileName, - int compressionLevel) +static unsigned long long +FIO_rust_compressZstdCallback(void* fCtx, void* prefs, void* ress, + const char* srcFileName, U64 srcFileSize, + int compressionLevel, U64* readsize) { - UTIL_time_t const timeStart = UTIL_getTime(); - clock_t const cpuStart = clock(); - U64 readsize = 0; - U64 compressedfilesize = 0; - U64 const fileSize = UTIL_getFileSize(srcFileName); - DISPLAYLEVEL(5, "%s: %llu bytes \n", srcFileName, (unsigned long long)fileSize); + return FIO_compressZstdFrame((FIO_ctx_t*)fCtx, (FIO_prefs_t*)prefs, + (const cRess_t*)ress, srcFileName, + srcFileSize, compressionLevel, readsize); +} - /* compression format selection */ - switch (prefs->compressionType) { - default: - case FIO_zstdCompression: - compressedfilesize = FIO_compressZstdFrame(fCtx, prefs, &ress, srcFileName, fileSize, compressionLevel, &readsize); - break; - - case FIO_gzipCompression: #ifdef ZSTD_GZCOMPRESS - compressedfilesize = FIO_compressGzFrame(&ress, srcFileName, fileSize, compressionLevel, &readsize); -#else - (void)compressionLevel; - EXM_THROW(20, "zstd: %s: file cannot be compressed as gzip (zstd compiled without ZSTD_GZCOMPRESS) -- ignored \n", - srcFileName); +static unsigned long long +FIO_rust_compressGzipCallback(void* ress, const char* srcFileName, + U64 srcFileSize, int compressionLevel, + U64* readsize) +{ + return FIO_compressGzFrame((const cRess_t*)ress, srcFileName, srcFileSize, + compressionLevel, readsize); +} #endif - break; - case FIO_xzCompression: - case FIO_lzmaCompression: #ifdef ZSTD_LZMACOMPRESS - compressedfilesize = FIO_compressLzmaFrame(&ress, srcFileName, fileSize, compressionLevel, &readsize, prefs->compressionType==FIO_lzmaCompression); -#else - (void)compressionLevel; - EXM_THROW(20, "zstd: %s: file cannot be compressed as xz/lzma (zstd compiled without ZSTD_LZMACOMPRESS) -- ignored \n", - srcFileName); +static unsigned long long +FIO_rust_compressLzmaCallback(void* ress, const char* srcFileName, + U64 srcFileSize, int compressionLevel, + U64* readsize, int plainLzma) +{ + return FIO_compressLzmaFrame((cRess_t*)ress, srcFileName, srcFileSize, + compressionLevel, readsize, plainLzma); +} #endif - break; - case FIO_lz4Compression: #ifdef ZSTD_LZ4COMPRESS - compressedfilesize = FIO_compressLz4Frame(&ress, srcFileName, fileSize, compressionLevel, prefs->checksumFlag, &readsize); -#else - (void)compressionLevel; - EXM_THROW(20, "zstd: %s: file cannot be compressed as lz4 (zstd compiled without ZSTD_LZ4COMPRESS) -- ignored \n", - srcFileName); +static unsigned long long +FIO_rust_compressLz4Callback(void* ress, const char* srcFileName, + U64 srcFileSize, int compressionLevel, + int checksumFlag, U64* readsize) +{ + return FIO_compressLz4Frame((cRess_t*)ress, srcFileName, srcFileSize, + compressionLevel, checksumFlag, readsize); +} #endif - break; - } - /* Status */ - fCtx->totalBytesInput += (size_t)readsize; - fCtx->totalBytesOutput += (size_t)compressedfilesize; +typedef struct { + UTIL_time_t timeStart; + clock_t cpuStart; +} FIO_compressDisplayContext_t; + +static void +FIO_rust_compressInputDisplayCallback(void* opaque, const char* srcFileName, + U64 fileSize) +{ + (void)opaque; + DISPLAYLEVEL(5, "%s: %llu bytes \n", srcFileName, + (unsigned long long)fileSize); +} + +static void +FIO_rust_compressStatusDisplayCallback(void* opaque, void* fCtxOpaque, + const char* dstFileName, + const char* srcFileName, + U64 readsize, U64 compressedfilesize) +{ + FIO_compressDisplayContext_t const* const displayCtx = + (const FIO_compressDisplayContext_t*)opaque; + FIO_ctx_t* const fCtx = (FIO_ctx_t*)fCtxOpaque; + DISPLAY_PROGRESS("\r%79s\r", ""); if (FIO_shouldDisplayFileSummary(fCtx)) { - UTIL_HumanReadableSize_t hr_isize = UTIL_makeHumanReadableSize((U64) readsize); - UTIL_HumanReadableSize_t hr_osize = UTIL_makeHumanReadableSize((U64) compressedfilesize); + UTIL_HumanReadableSize_t hr_isize = + UTIL_makeHumanReadableSize((U64)readsize); + UTIL_HumanReadableSize_t hr_osize = + UTIL_makeHumanReadableSize((U64)compressedfilesize); if (readsize == 0) { DISPLAY_SUMMARY("%-20s : (%6.*f%s => %6.*f%s, %s) \n", srcFileName, @@ -1713,16 +1764,75 @@ FIO_compressFilename_internal(FIO_ctx_t* const fCtx, } } - /* Elapsed Time and CPU Load */ + /* Keep the original elapsed-time and CPU-load diagnostics in C so their + * formatting and clock source remain identical to the old loop. */ { clock_t const cpuEnd = clock(); - double const cpuLoad_s = (double)(cpuEnd - cpuStart) / CLOCKS_PER_SEC; - U64 const timeLength_ns = UTIL_clockSpanNano(timeStart); + double const cpuLoad_s = (double)(cpuEnd - displayCtx->cpuStart) / CLOCKS_PER_SEC; + U64 const timeLength_ns = UTIL_clockSpanNano(displayCtx->timeStart); double const timeLength_s = (double)timeLength_ns / 1000000000; double const cpuLoad_pct = (cpuLoad_s / timeLength_s) * 100; DISPLAYLEVEL(4, "%-20s : Completed in %.2f sec (cpu load : %.0f%%)\n", - srcFileName, timeLength_s, cpuLoad_pct); + srcFileName, timeLength_s, cpuLoad_pct); + } +} + +/*! FIO_compressFilename_internal() : + * same as FIO_compressFilename_extRess(), with `ress.desFile` already opened. + * @return : 0 : compression completed correctly, + * 1 : missing or pb opening srcFileName + */ +static int +FIO_compressFilename_internal(FIO_ctx_t* const fCtx, + FIO_prefs_t* const prefs, + cRess_t ress, + const char* dstFileName, const char* srcFileName, + int compressionLevel) +{ + FIO_compressDisplayContext_t displayCtx = { + UTIL_getTime(), + clock() + }; + U64 const fileSize = UTIL_getFileSize(srcFileName); + FIO_rust_compress_callbacks_t callbacks; + int status; + + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.opaque = &displayCtx; + callbacks.compress_zstd = FIO_rust_compressZstdCallback; +#ifdef ZSTD_GZCOMPRESS + callbacks.compress_gzip = FIO_rust_compressGzipCallback; +#endif +#ifdef ZSTD_LZMACOMPRESS + callbacks.compress_lzma = FIO_rust_compressLzmaCallback; +#endif +#ifdef ZSTD_LZ4COMPRESS + callbacks.compress_lz4 = FIO_rust_compressLz4Callback; +#endif + callbacks.display_input = FIO_rust_compressInputDisplayCallback; + callbacks.display_status = FIO_rust_compressStatusDisplayCallback; + + status = FIO_rust_compressFilenameInternal( + fCtx, prefs, &ress, dstFileName, srcFileName, fileSize, + compressionLevel, &callbacks); + + switch (status) { + case FIO_RUST_COMPRESS_OK: + return 0; + case FIO_RUST_COMPRESS_GZIP_UNSUPPORTED: + EXM_THROW(20, "zstd: %s: file cannot be compressed as gzip (zstd compiled without ZSTD_GZCOMPRESS) -- ignored \n", + srcFileName); + case FIO_RUST_COMPRESS_LZMA_UNSUPPORTED: + EXM_THROW(20, "zstd: %s: file cannot be compressed as xz/lzma (zstd compiled without ZSTD_LZMACOMPRESS) -- ignored \n", + srcFileName); + case FIO_RUST_COMPRESS_LZ4_UNSUPPORTED: + EXM_THROW(20, "zstd: %s: file cannot be compressed as lz4 (zstd compiled without ZSTD_LZ4COMPRESS) -- ignored \n", + srcFileName); + case FIO_RUST_COMPRESS_ZSTD_UNSUPPORTED: + default: + assert(status == FIO_RUST_COMPRESS_ZSTD_UNSUPPORTED); + EXM_THROW(20, "zstd: %s: file cannot be compressed as zstd -- ignored \n", + srcFileName); } - return 0; } diff --git a/rust/src/fileio_asyncio.rs b/rust/src/fileio_asyncio.rs index bab6a43d4..965914f74 100644 --- a/rust/src/fileio_asyncio.rs +++ b/rust/src/fileio_asyncio.rs @@ -75,6 +75,52 @@ pub struct FIO_rust_decompress_callbacks_t { pub pass_through: Option, } +pub const FIO_RUST_COMPRESS_OK: c_int = 0; +pub const FIO_RUST_COMPRESS_GZIP_UNSUPPORTED: c_int = 1; +pub const FIO_RUST_COMPRESS_LZMA_UNSUPPORTED: c_int = 2; +pub const FIO_RUST_COMPRESS_LZ4_UNSUPPORTED: c_int = 3; +pub const FIO_RUST_COMPRESS_ZSTD_UNSUPPORTED: c_int = 4; + +const FIO_ZSTD_COMPRESSION: c_int = 0; +const FIO_GZIP_COMPRESSION: c_int = 1; +const FIO_XZ_COMPRESSION: c_int = 2; +const FIO_LZMA_COMPRESSION: c_int = 3; +const FIO_LZ4_COMPRESSION: c_int = 4; + +pub type FIO_rust_compress_zstd_fn = unsafe extern "C" fn( + *mut c_void, + *mut c_void, + *mut c_void, + *const c_char, + u64, + c_int, + *mut u64, +) -> u64; +pub type FIO_rust_compress_gzip_fn = + unsafe extern "C" fn(*mut c_void, *const c_char, u64, c_int, *mut u64) -> u64; +pub type FIO_rust_compress_lzma_fn = + unsafe extern "C" fn(*mut c_void, *const c_char, u64, c_int, *mut u64, c_int) -> u64; +pub type FIO_rust_compress_lz4_fn = + unsafe extern "C" fn(*mut c_void, *const c_char, u64, c_int, c_int, *mut u64) -> u64; +pub type FIO_rust_compress_input_display_fn = unsafe extern "C" fn(*mut c_void, *const c_char, u64); +pub type FIO_rust_compress_status_display_fn = + unsafe extern "C" fn(*mut c_void, *mut c_void, *const c_char, *const c_char, u64, u64); + +/// C-owned compression codecs and CLI display hooks used by the Rust file +/// compression selector. The codec callbacks deliberately receive opaque +/// resource pointers: Rust owns the selection/accounting loop but never +/// assumes the private `cRess_t` layout. +#[repr(C)] +pub struct FIO_rust_compress_callbacks_t { + pub opaque: *mut c_void, + pub compress_zstd: Option, + pub compress_gzip: Option, + pub compress_lzma: Option, + pub compress_lz4: Option, + pub display_input: Option, + pub display_status: Option, +} + /// C's `FIO_prefs_t` from `programs/fileio_types.h`. /// /// `fileio_prefs.rs` contains the same C layout for the preferences API. It @@ -145,6 +191,20 @@ pub struct IOJob_t { pub offset: u64, } +/// The compression selector updates only these public counters in C's +/// private `FIO_ctx_s`. The field order is kept explicit so no C-owned +/// context implementation details cross the callback boundary. +#[repr(C)] +struct FIO_rust_compression_context_t { + nbFilesTotal: c_int, + hasStdinInput: c_int, + hasStdoutOutput: c_int, + currFileIdx: c_int, + nbFilesProcessed: c_int, + totalBytesInput: usize, + totalBytesOutput: usize, +} + type PoolFunction = unsafe extern "C" fn(*mut c_void); #[cfg(not(test))] @@ -1437,6 +1497,173 @@ pub unsafe extern "C" fn FIO_rust_passThrough( 0 } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CompressionFormat { + Zstd, + Gzip, + Lzma { plain_lzma: c_int }, + Lz4 { checksum: c_int }, +} + +fn select_compression_format(compression_type: c_int, checksum: c_int) -> CompressionFormat { + match compression_type { + FIO_GZIP_COMPRESSION => CompressionFormat::Gzip, + FIO_XZ_COMPRESSION => CompressionFormat::Lzma { plain_lzma: 0 }, + FIO_LZMA_COMPRESSION => CompressionFormat::Lzma { plain_lzma: 1 }, + FIO_LZ4_COMPRESSION => CompressionFormat::Lz4 { checksum }, + FIO_ZSTD_COMPRESSION => CompressionFormat::Zstd, + _ => CompressionFormat::Zstd, + } +} + +unsafe fn run_compression_callback( + f_ctx: *mut c_void, + prefs: *mut FIO_prefs_t, + ress: *mut c_void, + src_file_name: *const c_char, + file_size: u64, + compression_level: c_int, + callbacks: &FIO_rust_compress_callbacks_t, +) -> Result<(u64, u64), c_int> { + let format = select_compression_format(unsafe { (*prefs).compressionType }, unsafe { + (*prefs).checksumFlag + }); + let mut read_size = 0_u64; + + let compressed_size = match format { + CompressionFormat::Zstd => { + let Some(callback) = callbacks.compress_zstd else { + return Err(FIO_RUST_COMPRESS_ZSTD_UNSUPPORTED); + }; + unsafe { + callback( + f_ctx, + prefs.cast(), + ress, + src_file_name, + file_size, + compression_level, + &mut read_size, + ) + } + } + CompressionFormat::Gzip => { + let Some(callback) = callbacks.compress_gzip else { + return Err(FIO_RUST_COMPRESS_GZIP_UNSUPPORTED); + }; + unsafe { + callback( + ress, + src_file_name, + file_size, + compression_level, + &mut read_size, + ) + } + } + CompressionFormat::Lzma { plain_lzma } => { + let Some(callback) = callbacks.compress_lzma else { + return Err(FIO_RUST_COMPRESS_LZMA_UNSUPPORTED); + }; + unsafe { + callback( + ress, + src_file_name, + file_size, + compression_level, + &mut read_size, + plain_lzma, + ) + } + } + CompressionFormat::Lz4 { checksum } => { + let Some(callback) = callbacks.compress_lz4 else { + return Err(FIO_RUST_COMPRESS_LZ4_UNSUPPORTED); + }; + unsafe { + callback( + ress, + src_file_name, + file_size, + compression_level, + checksum, + &mut read_size, + ) + } + } + }; + + Ok((read_size, compressed_size)) +} + +/// Runs the compression-side format selection and final file accounting. +/// +/// The codec implementations remain in C and are selected through the +/// explicit callback table above. Rust updates the C-visible aggregate byte +/// counters only after a codec returns, then asks C to perform the existing +/// progress, summary, and elapsed-time display. Missing optional codecs are +/// returned as status values so the C wrapper can preserve its exact +/// `EXM_THROW()` diagnostics. +#[no_mangle] +pub unsafe extern "C" fn FIO_rust_compressFilenameInternal( + f_ctx: *mut c_void, + prefs: *mut FIO_prefs_t, + ress: *mut c_void, + dst_file_name: *const c_char, + src_file_name: *const c_char, + file_size: u64, + compression_level: c_int, + callbacks: *const FIO_rust_compress_callbacks_t, +) -> c_int { + assert!(!f_ctx.is_null()); + assert!(!prefs.is_null()); + assert!(!ress.is_null()); + assert!(!dst_file_name.is_null()); + assert!(!src_file_name.is_null()); + assert!(!callbacks.is_null()); + + let callbacks = unsafe { &*callbacks }; + if let Some(display) = callbacks.display_input { + unsafe { display(callbacks.opaque, src_file_name, file_size) }; + } + + let (read_size, compressed_size) = match unsafe { + run_compression_callback( + f_ctx, + prefs, + ress, + src_file_name, + file_size, + compression_level, + callbacks, + ) + } { + Ok(sizes) => sizes, + Err(status) => return status, + }; + + let context = unsafe { &mut *f_ctx.cast::() }; + context.totalBytesInput = context.totalBytesInput.wrapping_add(read_size as usize); + context.totalBytesOutput = context + .totalBytesOutput + .wrapping_add(compressed_size as usize); + + if let Some(display) = callbacks.display_status { + unsafe { + display( + callbacks.opaque, + f_ctx, + dst_file_name, + src_file_name, + read_size, + compressed_size, + ) + }; + } + + FIO_RUST_COMPRESS_OK +} + /// Decompresses exactly one zstd frame using the existing asynchronous pools. /// /// C retains the frame dispatcher and all user-facing diagnostics. This ABI @@ -1896,6 +2123,269 @@ mod tests { prefs } + #[derive(Default)] + struct CompressionCallbackState { + zstd_calls: usize, + gzip_calls: usize, + lzma_calls: usize, + lz4_calls: usize, + lzma_plain: c_int, + lz4_checksum: c_int, + } + + unsafe extern "C" fn record_compress_zstd( + _f_ctx: *mut c_void, + _prefs: *mut c_void, + ress: *mut c_void, + _src_file_name: *const c_char, + _file_size: u64, + _compression_level: c_int, + read_size: *mut u64, + ) -> u64 { + let state = unsafe { &mut *ress.cast::() }; + state.zstd_calls += 1; + unsafe { *read_size = 11 }; + 7 + } + + unsafe extern "C" fn record_compress_gzip( + ress: *mut c_void, + _src_file_name: *const c_char, + _file_size: u64, + _compression_level: c_int, + read_size: *mut u64, + ) -> u64 { + let state = unsafe { &mut *ress.cast::() }; + state.gzip_calls += 1; + unsafe { *read_size = 12 }; + 8 + } + + unsafe extern "C" fn record_compress_lzma( + ress: *mut c_void, + _src_file_name: *const c_char, + _file_size: u64, + _compression_level: c_int, + read_size: *mut u64, + plain_lzma: c_int, + ) -> u64 { + let state = unsafe { &mut *ress.cast::() }; + state.lzma_calls += 1; + state.lzma_plain = plain_lzma; + unsafe { *read_size = 13 }; + 9 + } + + unsafe extern "C" fn record_compress_lz4( + ress: *mut c_void, + _src_file_name: *const c_char, + _file_size: u64, + _compression_level: c_int, + checksum: c_int, + read_size: *mut u64, + ) -> u64 { + let state = unsafe { &mut *ress.cast::() }; + state.lz4_calls += 1; + state.lz4_checksum = checksum; + unsafe { *read_size = 14 }; + 10 + } + + fn compression_test_callbacks() -> FIO_rust_compress_callbacks_t { + FIO_rust_compress_callbacks_t { + opaque: ptr::null_mut(), + compress_zstd: Some(record_compress_zstd), + compress_gzip: Some(record_compress_gzip), + compress_lzma: Some(record_compress_lzma), + compress_lz4: Some(record_compress_lz4), + display_input: None, + display_status: None, + } + } + + #[derive(Default)] + struct CompressionDisplayState { + input_sizes: Vec, + status_sizes: Vec<(u64, u64)>, + } + + unsafe extern "C" fn record_compress_input( + opaque: *mut c_void, + _src_file_name: *const c_char, + file_size: u64, + ) { + let state = unsafe { &mut *opaque.cast::() }; + state.input_sizes.push(file_size); + } + + unsafe extern "C" fn record_compress_status( + opaque: *mut c_void, + _f_ctx: *mut c_void, + _dst_file_name: *const c_char, + _src_file_name: *const c_char, + read_size: u64, + compressed_size: u64, + ) { + let state = unsafe { &mut *opaque.cast::() }; + state.status_sizes.push((read_size, compressed_size)); + } + + #[test] + fn compression_selection_dispatches_each_format_and_forwards_options() { + let source = c"source"; + let destination = c"destination"; + let cases = [ + (99, 1, 0_usize, 0, 11_u64, 7_u64), + (FIO_ZSTD_COMPRESSION, 1, 0, 0, 11, 7), + (FIO_GZIP_COMPRESSION, 1, 1, 0, 12, 8), + (FIO_XZ_COMPRESSION, 1, 2, 0, 13, 9), + (FIO_LZMA_COMPRESSION, 1, 2, 1, 13, 9), + (FIO_LZ4_COMPRESSION, 2, 3, 0, 14, 10), + ]; + + for ( + compression_type, + checksum, + expected_codec, + expected_plain, + expected_read, + expected_output, + ) in cases + { + let mut prefs: FIO_prefs_t = unsafe { std::mem::zeroed() }; + prefs.compressionType = compression_type; + prefs.checksumFlag = checksum; + let mut context = FIO_rust_compression_context_t { + nbFilesTotal: 1, + hasStdinInput: 0, + hasStdoutOutput: 0, + currFileIdx: 0, + nbFilesProcessed: 0, + totalBytesInput: 100, + totalBytesOutput: 200, + }; + let callbacks = compression_test_callbacks(); + let mut state = CompressionCallbackState::default(); + + assert_eq!( + unsafe { + FIO_rust_compressFilenameInternal( + (&mut context as *mut FIO_rust_compression_context_t).cast(), + &mut prefs, + (&mut state as *mut CompressionCallbackState).cast(), + destination.as_ptr(), + source.as_ptr(), + 123, + 5, + &callbacks, + ) + }, + FIO_RUST_COMPRESS_OK + ); + + assert_eq!( + [ + state.zstd_calls, + state.gzip_calls, + state.lzma_calls, + state.lz4_calls, + ][expected_codec], + 1 + ); + assert_eq!( + state.zstd_calls + state.gzip_calls + state.lzma_calls + state.lz4_calls, + 1 + ); + assert_eq!(state.lzma_plain, expected_plain); + assert_eq!( + state.lz4_checksum, + if expected_codec == 3 { checksum } else { 0 } + ); + assert_eq!(context.totalBytesInput, 100 + expected_read as usize); + assert_eq!(context.totalBytesOutput, 200 + expected_output as usize); + } + } + + #[test] + fn compression_accounting_calls_display_hooks_after_codec_success() { + let source = c"source"; + let destination = c"destination"; + let mut prefs: FIO_prefs_t = unsafe { std::mem::zeroed() }; + prefs.compressionType = FIO_ZSTD_COMPRESSION; + let mut context = FIO_rust_compression_context_t { + nbFilesTotal: 1, + hasStdinInput: 0, + hasStdoutOutput: 0, + currFileIdx: 0, + nbFilesProcessed: 0, + totalBytesInput: 0, + totalBytesOutput: 0, + }; + let mut codec_state = CompressionCallbackState::default(); + let mut display_state = CompressionDisplayState::default(); + let mut callbacks = compression_test_callbacks(); + callbacks.opaque = (&mut display_state as *mut CompressionDisplayState).cast(); + callbacks.display_input = Some(record_compress_input); + callbacks.display_status = Some(record_compress_status); + + assert_eq!( + unsafe { + FIO_rust_compressFilenameInternal( + (&mut context as *mut FIO_rust_compression_context_t).cast(), + &mut prefs, + (&mut codec_state as *mut CompressionCallbackState).cast(), + destination.as_ptr(), + source.as_ptr(), + 123, + 5, + &callbacks, + ) + }, + FIO_RUST_COMPRESS_OK + ); + assert_eq!(display_state.input_sizes, vec![123]); + assert_eq!(display_state.status_sizes, vec![(11, 7)]); + assert_eq!(context.totalBytesInput, 11); + assert_eq!(context.totalBytesOutput, 7); + } + + #[test] + fn compression_missing_optional_codec_leaves_accounting_unchanged() { + let source = c"source"; + let destination = c"destination"; + let mut prefs: FIO_prefs_t = unsafe { std::mem::zeroed() }; + prefs.compressionType = FIO_GZIP_COMPRESSION; + let mut context = FIO_rust_compression_context_t { + nbFilesTotal: 1, + hasStdinInput: 0, + hasStdoutOutput: 0, + currFileIdx: 0, + nbFilesProcessed: 0, + totalBytesInput: 31, + totalBytesOutput: 47, + }; + let mut callbacks = compression_test_callbacks(); + callbacks.compress_gzip = None; + + assert_eq!( + unsafe { + FIO_rust_compressFilenameInternal( + (&mut context as *mut FIO_rust_compression_context_t).cast(), + &mut prefs, + ptr::dangling_mut::(), + destination.as_ptr(), + source.as_ptr(), + 123, + 5, + &callbacks, + ) + }, + FIO_RUST_COMPRESS_GZIP_UNSUPPORTED + ); + assert_eq!(context.totalBytesInput, 31); + assert_eq!(context.totalBytesOutput, 47); + } + #[test] fn classifies_all_cli_decompression_headers() { let is_mock_zstd = |buffer: &[u8]| buffer.starts_with(&[0x28, 0xB5, 0x2F, 0xFD]);