#![allow(non_camel_case_types)] #![allow(non_snake_case)] #![allow(clippy::missing_safety_doc)] //! Rust command-line frontend for zstd. //! //! This is the Rust parser and dispatch layer plus selected file-I/O leaves, //! not yet a fully independent file-I/O implementation. It shares the //! mature C `fileio` layer through a narrow public-in-the-programs-tree ABI: //! Rust owns preference policy, filename decisions, file opening, dictionary //! loading, asynchronous pools, and pass-through copying, while C still owns //! format-specific dispatch, metadata preservation, and remaining streaming //! orchestration. //! //! Benchmark mode (`-b`) parses and dispatches through the Rust CLI archive. //! The bridge preserves the C CLI's argument normalization and uses the //! existing Rust `benchzstd` APIs. Builds without the `benchmark` feature //! return the same unavailable-build result as `ZSTD_NOBENCH` builds without //! referencing benchmark symbols. //! //! Recursive expansion (`-r`), `--filelist`, the output-directory modes, and //! `--list` reuse the C `FileNamesTable` helpers from `programs/util.c` and //! `FIO_listMultipleFiles`, so directory traversal and frame inspection stay //! byte-identical with the C CLI. //! //! Remaining C-only CLI boundaries are called out in `unsupported()` below: //! a few compatibility-only options. Trace logging uses the existing C trace //! backend through a narrow lifecycle bridge, while dictionary training uses //! the temporary `DiB_trainFromFiles` bridge and Rust-owned algorithms. use std::env; use std::ffi::{CStr, CString, OsStr, OsString}; use std::fs; use std::io::{self, IsTerminal, Write}; use std::os::raw::{c_char, c_int, c_uint}; use std::path::Path; use std::ptr; #[cfg(unix)] use std::os::unix::ffi::{OsStrExt, OsStringExt}; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; const DEFAULT_CLEVEL: i32 = 3; #[cfg(feature = "compression")] const DEFAULT_MAX_CLEVEL: i32 = 19; const DEFAULT_BENCH_NB_SECONDS: u32 = 3; const DEFAULT_MEM_LIMIT: u32 = 1 << 27; const DEFAULT_LONG_WINDOW_LOG: u32 = 27; const DEFAULT_DICT_NAME: &str = "dictionary"; const DEFAULT_MAX_DICT_SIZE: usize = 110 << 10; const DEFAULT_DICT_SELECTIVITY: u32 = 9; const DEFAULT_SHRINK_DICT_REGRESSION: u32 = 1; const DEFAULT_FASTCOVER_ACCEL: u32 = 1; const MAX_FAST_ACCELERATION: i32 = 128 << 10; #[cfg(target_pointer_width = "32")] const MAX_WINDOW_LOG: u32 = 30; #[cfg(not(target_pointer_width = "32"))] const MAX_WINDOW_LOG: u32 = 31; #[cfg(target_pointer_width = "32")] const MAX_CHAIN_LOG: u32 = 29; #[cfg(not(target_pointer_width = "32"))] const MAX_CHAIN_LOG: u32 = 30; const MAX_HASH_LOG: u32 = 30; const MAX_SEARCH_LOG: u32 = MAX_WINDOW_LOG - 1; const MAX_MIN_MATCH: u32 = 3; const MAX_TARGET_LENGTH: u32 = 1 << 17; const MAX_STRATEGY: c_int = 9; const MAX_OVERLAP_LOG: i32 = 9; const MAX_LDM_HASH_LOG: i32 = 30; const MAX_LDM_MIN_MATCH: i32 = 16; const MAX_LDM_BUCKET_SIZE_LOG: i32 = 8; const STDIN_MARK: &str = "/*stdin*\\"; const STDOUT_MARK: &str = "/*stdout*\\"; #[cfg(windows)] const NULL_MARK: &str = "NUL"; #[cfg(not(windows))] const NULL_MARK: &str = "/dev/null"; const ZSTD_SUFFIX: &[u8] = b".zst\0"; const GZ_SUFFIX: &[u8] = b".gz\0"; const XZ_SUFFIX: &[u8] = b".xz\0"; const LZMA_SUFFIX: &[u8] = b".lzma\0"; const LZ4_SUFFIX: &[u8] = b".lz4\0"; 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; const FIO_PS_AUTO: c_int = 0; const FIO_PS_NEVER: c_int = 1; const FIO_PS_ALWAYS: c_int = 2; const ZSTD_PS_AUTO: c_int = 0; const ZSTD_PS_ENABLE: c_int = 1; const ZSTD_PS_DISABLE: c_int = 2; #[repr(C)] struct FIO_prefs_t { _private: [u8; 0], } #[repr(C)] struct FIO_ctx_t { _private: [u8; 0], } #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] pub(crate) struct ZSTD_compressionParameters { windowLog: u32, chainLog: u32, hashLog: u32, searchLog: u32, minMatch: u32, targetLength: u32, strategy: c_int, } #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] struct ZDICT_params_t { compressionLevel: c_int, notificationLevel: c_uint, dictID: c_uint, } #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] struct ZDICT_cover_params_t { k: c_uint, d: c_uint, steps: c_uint, nbThreads: c_uint, splitPoint: f64, shrinkDict: c_uint, shrinkDictMaxRegression: c_uint, zParams: ZDICT_params_t, } #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] struct ZDICT_fastCover_params_t { k: c_uint, d: c_uint, f: c_uint, steps: c_uint, nbThreads: c_uint, splitPoint: f64, accel: c_uint, shrinkDict: c_uint, shrinkDictMaxRegression: c_uint, zParams: ZDICT_params_t, } #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg(all(feature = "compression", feature = "dict-builder"))] struct ZDICT_legacy_params_t { selectivityLevel: c_uint, zParams: ZDICT_params_t, } /// Mirror of the `FileNamesTable` in `programs/util.h`; the tables returned /// by the `UTIL_*FNT` helpers are only read and released here, never resized. #[repr(C)] struct FileNamesTable { fileNames: *mut *const c_char, buf: *mut c_char, tableSize: usize, tableCapacity: usize, } unsafe extern "C" { fn ZSTD_versionString() -> *const c_char; fn ZSTD_rust_cli_expected_version() -> *const c_char; static mut g_utilDisplayLevel: c_int; #[cfg(feature = "compression")] fn ZSTD_minCLevel() -> c_int; #[cfg(feature = "compression")] fn ZSTD_maxCLevel() -> c_int; #[cfg(feature = "compression")] fn UTIL_countPhysicalCores() -> c_int; #[cfg(feature = "compression")] fn UTIL_countLogicalCores() -> c_int; fn UTIL_getFileSize(input_file_name: *const c_char) -> u64; fn UTIL_isConsole(file: *mut libc::FILE) -> c_int; fn UTIL_isLink(input_file_name: *const c_char) -> c_int; fn UTIL_fakeStderrIsConsole(); fn UTIL_traceFileStat(); static mut stdin: *mut libc::FILE; static mut stdout: *mut libc::FILE; static mut stderr: *mut libc::FILE; fn UTIL_createFileNamesTable_fromFileName( input_file_name: *const c_char, ) -> *mut FileNamesTable; fn UTIL_createExpandedFNT( file_names: *const *const c_char, nb_file_names: usize, follow_links: c_int, ) -> *mut FileNamesTable; fn UTIL_freeFileNamesTable(table: *mut FileNamesTable); fn FIO_createPreferences() -> *mut FIO_prefs_t; fn FIO_freePreferences(prefs: *mut FIO_prefs_t); fn FIO_createContext() -> *mut FIO_ctx_t; fn FIO_freeContext(ctx: *mut FIO_ctx_t); fn FIO_addAbortHandler(); fn FIO_setCompressionType(prefs: *mut FIO_prefs_t, compression_type: c_int); fn FIO_overwriteMode(prefs: *mut FIO_prefs_t); fn FIO_setAdaptiveMode(prefs: *mut FIO_prefs_t, adapt: c_int); #[cfg(feature = "compression")] fn FIO_setAdaptMin(prefs: *mut FIO_prefs_t, level: c_int); #[cfg(feature = "compression")] fn FIO_setAdaptMax(prefs: *mut FIO_prefs_t, level: c_int); fn FIO_setUseRowMatchFinder(prefs: *mut FIO_prefs_t, mode: c_int); fn FIO_setBlockSize(prefs: *mut FIO_prefs_t, block_size: c_int); fn FIO_setChecksumFlag(prefs: *mut FIO_prefs_t, checksum: c_int); fn FIO_setDictIDFlag(prefs: *mut FIO_prefs_t, dict_id: c_int); fn FIO_setLdmBucketSizeLog(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setLdmFlag(prefs: *mut FIO_prefs_t, value: c_uint); fn FIO_setLdmHashRateLog(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setLdmHashLog(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setLdmMinMatch(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setMemLimit(prefs: *mut FIO_prefs_t, limit: c_uint); #[cfg(feature = "compression")] fn FIO_setNbWorkers(prefs: *mut FIO_prefs_t, workers: c_int); fn FIO_setOverlapLog(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setRemoveSrcFile(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setSparseWrite(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setRsyncable(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setStreamSrcSize(prefs: *mut FIO_prefs_t, value: usize); fn FIO_setTargetCBlockSize(prefs: *mut FIO_prefs_t, value: usize); fn FIO_setSrcSizeHint(prefs: *mut FIO_prefs_t, value: usize); #[cfg(feature = "decompression")] fn FIO_setTestMode(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setLiteralCompressionMode(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setProgressSetting(value: c_int); fn FIO_setNotificationLevel(value: c_int); fn FIO_setExcludeCompressedFile(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setAllowBlockDevices(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setContentSize(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setAsyncIOFlag(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setPassThroughFlag(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setPatchFromMode(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setMMapDict(prefs: *mut FIO_prefs_t, value: c_int); fn FIO_setNbFilesTotal(ctx: *mut FIO_ctx_t, value: c_int); fn FIO_setHasStdinInput(ctx: *mut FIO_ctx_t, value: c_int); fn FIO_setHasStdoutOutput(ctx: *mut FIO_ctx_t, value: c_int); fn TRACE_enable(filename: *const c_char); fn TRACE_finish(); #[cfg(feature = "compression")] fn FIO_compressFilename( ctx: *mut FIO_ctx_t, prefs: *mut FIO_prefs_t, output: *const c_char, input: *const c_char, dict: *const c_char, level: c_int, params: ZSTD_compressionParameters, ) -> c_int; #[cfg(feature = "decompression")] fn FIO_decompressFilename( ctx: *mut FIO_ctx_t, prefs: *mut FIO_prefs_t, output: *const c_char, input: *const c_char, dict: *const c_char, ) -> c_int; #[cfg(feature = "compression")] fn FIO_compressMultipleFilenames( ctx: *mut FIO_ctx_t, prefs: *mut FIO_prefs_t, inputs: *const *const c_char, output_mirror_dir: *const c_char, output_dir: *const c_char, output: *const c_char, suffix: *const c_char, dict: *const c_char, level: c_int, params: ZSTD_compressionParameters, ) -> c_int; #[cfg(feature = "decompression")] fn FIO_decompressMultipleFilenames( ctx: *mut FIO_ctx_t, prefs: *mut FIO_prefs_t, inputs: *const *const c_char, output_mirror_dir: *const c_char, output_dir: *const c_char, output: *const c_char, dict: *const c_char, ) -> c_int; #[cfg(feature = "decompression")] fn FIO_listMultipleFiles( nb_files: c_uint, file_names: *const *const c_char, display_level: c_int, ) -> c_int; /// Narrow bridge to `programs/dibio.c`. The file loader and dictionary /// algorithms remain on the C/Rust library side of this boundary. #[cfg(all(feature = "compression", feature = "dict-builder"))] fn DiB_trainFromFiles( dict_file_name: *const c_char, max_dict_size: usize, file_names: *const *const c_char, nb_files: c_int, chunk_size: usize, legacy_params: *mut ZDICT_legacy_params_t, cover_params: *mut ZDICT_cover_params_t, fast_cover_params: *mut ZDICT_fastCover_params_t, optimize: c_int, mem_limit: c_uint, ) -> c_int; #[cfg(feature = "compression")] fn ZSTD_getCParams( compression_level: c_int, estimated_src_size: u64, dict_size: usize, ) -> ZSTD_compressionParameters; } const ZSTD_STRATEGY_NAMES: [&str; 10] = [ "", "ZSTD_fast", "ZSTD_dfast", "ZSTD_greedy", "ZSTD_lazy", "ZSTD_lazy2", "ZSTD_btlazy2", "ZSTD_btopt", "ZSTD_btultra", "ZSTD_btultra2", ]; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Operation { Compress, Decompress, Test, Bench, List, Train, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum CompressionFormat { Zstd, Gzip, Xz, Lzma, Lz4, } impl CompressionFormat { fn parse(value: &str) -> Result { match value { "zstd" => Ok(Self::Zstd), "gzip" => Ok(Self::Gzip), "xz" => Ok(Self::Xz), "lzma" => Ok(Self::Lzma), "lz4" => Ok(Self::Lz4), _ => Err(format!("unknown compression format {value:?}")), } } const fn fio_type(self) -> c_int { match self { Self::Zstd => FIO_ZSTD_COMPRESSION, Self::Gzip => FIO_GZIP_COMPRESSION, Self::Xz => FIO_XZ_COMPRESSION, Self::Lzma => FIO_LZMA_COMPRESSION, Self::Lz4 => FIO_LZ4_COMPRESSION, } } const fn suffix(self) -> &'static [u8] { match self { Self::Zstd => ZSTD_SUFFIX, Self::Gzip => GZ_SUFFIX, Self::Xz => XZ_SUFFIX, Self::Lzma => LZMA_SUFFIX, Self::Lz4 => LZ4_SUFFIX, } } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] enum TrainingAlgorithm { Cover, #[default] FastCover, Legacy, } #[derive(Debug)] enum Action { Run(Box), Help { advanced: bool }, Version { quiet: bool }, } #[derive(Debug)] struct Cli { operation: Operation, bench_decode: bool, compression_format: CompressionFormat, inputs: Vec, output: Option, dictionary: Option, patch_from: Option, level: i32, ultra: bool, display_level: i32, force: bool, force_stdout: bool, remove_source: bool, checksum: Option, sparse: Option, pass_through: Option, content_size: i32, dict_id: Option, async_io: Option, mmap_dict: i32, progress: i32, workers: Option, single_thread: bool, auto_threads_logical: bool, block_size: Option, mem_limit: Option, ldm: bool, ldm_hash_log: Option, ldm_min_match: Option, ldm_bucket_size_log: Option, ldm_hash_rate_log: Option, overlap_log: Option, adapt: bool, adapt_min: Option, adapt_max: Option, rsyncable: bool, stream_src_size: Option, target_cblock_size: Option, src_size_hint: Option, literal_compression: Option, row_match_finder: i32, exclude_compressed: bool, compression_params: ZSTD_compressionParameters, show_default_cparams: bool, bench_end_level: Option, bench_nb_seconds: Option, recursive: bool, file_lists: Vec, output_dir_flat: Option, output_dir_mirror: Option, training_algorithm: TrainingAlgorithm, max_dict_size: usize, dictionary_id: u32, dictionary_selectivity: u32, trace: Option, fake_stderr_is_console: bool, trace_file_stat: bool, gzip_compat: bool, cover_params: ZDICT_cover_params_t, fast_cover_params: ZDICT_fastCover_params_t, } impl Cli { fn new(program_name: &str) -> Self { let mut cli = Self { operation: Operation::Compress, bench_decode: false, compression_format: CompressionFormat::Zstd, inputs: Vec::new(), output: None, dictionary: None, patch_from: None, level: default_level(), ultra: false, display_level: 2, force: false, force_stdout: false, remove_source: false, checksum: None, sparse: None, pass_through: None, content_size: 1, dict_id: None, async_io: None, mmap_dict: ZSTD_PS_AUTO, progress: FIO_PS_AUTO, workers: None, single_thread: false, auto_threads_logical: false, block_size: None, mem_limit: None, ldm: false, ldm_hash_log: None, ldm_min_match: None, ldm_bucket_size_log: None, ldm_hash_rate_log: None, overlap_log: None, adapt: false, adapt_min: None, adapt_max: None, rsyncable: false, stream_src_size: None, target_cblock_size: None, src_size_hint: None, literal_compression: None, row_match_finder: ZSTD_PS_AUTO, exclude_compressed: false, compression_params: ZSTD_compressionParameters::default(), show_default_cparams: false, bench_end_level: None, bench_nb_seconds: None, recursive: false, file_lists: Vec::new(), output_dir_flat: None, output_dir_mirror: None, training_algorithm: TrainingAlgorithm::default(), max_dict_size: DEFAULT_MAX_DICT_SIZE, dictionary_id: 0, dictionary_selectivity: DEFAULT_DICT_SELECTIVITY, trace: None, fake_stderr_is_console: false, trace_file_stat: false, gzip_compat: false, cover_params: ZDICT_cover_params_t::default(), fast_cover_params: default_fast_cover_params(), }; match program_name { "unzstd" => cli.operation = Operation::Decompress, "zstdmt" => cli.workers = Some(0), "zstdcat" | "zcat" => { cli.operation = Operation::Decompress; cli.output = Some(cstring(STDOUT_MARK).expect("static stdout marker")); cli.force = true; cli.force_stdout = true; cli.pass_through = Some(1); cli.display_level = 1; } "gzip" => { cli.compression_format = CompressionFormat::Gzip; cli.remove_source = true; cli.gzip_compat = true; } "gunzip" => { cli.compression_format = CompressionFormat::Gzip; cli.operation = Operation::Decompress; cli.remove_source = true; } "gzcat" => { cli.compression_format = CompressionFormat::Gzip; cli.operation = Operation::Decompress; cli.output = Some(cstring(STDOUT_MARK).expect("static stdout marker")); cli.force = true; cli.force_stdout = true; cli.pass_through = Some(1); cli.display_level = 1; } "xz" => { cli.compression_format = CompressionFormat::Xz; cli.remove_source = true; } "unxz" => { cli.compression_format = CompressionFormat::Xz; cli.operation = Operation::Decompress; cli.remove_source = true; } "lzma" => { cli.compression_format = CompressionFormat::Lzma; cli.remove_source = true; } "unlzma" => { cli.compression_format = CompressionFormat::Lzma; cli.operation = Operation::Decompress; cli.remove_source = true; } "lz4" => cli.compression_format = CompressionFormat::Lz4, "unlz4" => { cli.compression_format = CompressionFormat::Lz4; cli.operation = Operation::Decompress; } _ => {} } cli } } fn cstring(value: &str) -> Result { CString::new(value).map_err(|_| format!("argument contains an interior NUL: {value:?}")) } fn os_cstring(value: &OsStr) -> Result { #[cfg(unix)] { CString::new(value.as_bytes()).map_err(|_| "file name contains an interior NUL".to_owned()) } #[cfg(not(unix))] { cstring(&value.to_string_lossy()) } } fn default_level() -> i32 { let Ok(value) = env::var("ZSTD_CLEVEL") else { return DEFAULT_CLEVEL; }; match value.parse::() { Ok(level) if (i32::MIN as i128..=i32::MAX as i128).contains(&level) => level as i32, Ok(_) => { eprintln!( "Ignore environment variable setting ZSTD_CLEVEL={value}: numeric value too large " ); DEFAULT_CLEVEL } Err(_) if value_has_only_sign_and_digits(&value) => { eprintln!( "Ignore environment variable setting ZSTD_CLEVEL={value}: numeric value too large " ); DEFAULT_CLEVEL } Err(_) => { eprintln!( "Ignore environment variable setting ZSTD_CLEVEL={value}: not a valid integer value " ); DEFAULT_CLEVEL } } } fn value_has_only_sign_and_digits(value: &str) -> bool { let digits = value .strip_prefix('+') .or_else(|| value.strip_prefix('-')) .unwrap_or(value); !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) } #[cfg(feature = "compression")] unsafe fn default_worker_count() -> i32 { if let Ok(value) = env::var("ZSTD_NBTHREADS") { if let Ok(workers) = value.parse::() { if let Ok(workers) = i32::try_from(workers) { return workers; } } } let logical_cores = unsafe { UTIL_countLogicalCores() }.max(1); (logical_cores / 4).clamp(1, 4) } #[cfg(feature = "compression")] unsafe fn resolved_worker_count( workers: Option, single_thread: bool, auto_threads_logical: bool, ) -> i32 { match workers { /* --single-thread pins zero workers; a bare zero (-T0 or the zstdmt * program name) auto-detects the core count as in the C CLI. */ Some(0) if single_thread => 0, Some(0) if auto_threads_logical => unsafe { UTIL_countLogicalCores() }.max(1), Some(0) => unsafe { UTIL_countPhysicalCores() }.max(1), Some(workers) => workers, None => unsafe { default_worker_count() }, } } fn program_basename(value: &OsStr) -> String { Path::new(value) .file_name() .unwrap_or(value) .to_string_lossy() .split('.') .next() .unwrap_or("zstd") .to_owned() } #[cfg(feature = "compression")] fn max_c_level_for_help() -> i32 { DEFAULT_MAX_CLEVEL } #[cfg(not(feature = "compression"))] const fn max_c_level_for_help() -> i32 { 19 } #[cfg(feature = "compression")] fn max_c_level_for_ultra() -> i32 { unsafe { ZSTD_maxCLevel() } } #[cfg(not(feature = "compression"))] const fn max_c_level_for_ultra() -> i32 { 19 } fn write_basic_usage(out: &mut W, program_name: &str) { let max_level = max_c_level_for_help(); let _ = writeln!( out, "Compress or decompress the INPUT file(s); reads from STDIN if INPUT is `-` or not provided." ); let _ = writeln!( out, "\nUsage: {program_name} [OPTIONS...] [INPUT... | -] [-o OUTPUT]" ); let _ = writeln!(out, "\nOptions:"); let _ = writeln!( out, " -o OUTPUT Write output to a single file, OUTPUT." ); let _ = writeln!( out, " -k, --keep Preserve INPUT file(s). [Default] " ); let _ = writeln!( out, " --rm Remove INPUT file(s) after successful (de)compression." ); if program_name == "gzip" { let _ = writeln!( out, " -n, --no-name Do not store original filename when compressing." ); } let _ = writeln!(out); let _ = writeln!( out, " -# Desired compression level, where `#` is a number between 1 and {max_level};" ); let _ = writeln!( out, " lower numbers provide faster compression, higher numbers yield" ); let _ = writeln!( out, " better compression ratios. [Default: {DEFAULT_CLEVEL}]" ); let _ = writeln!(out); let _ = writeln!( out, " -d, --decompress Perform decompression." ); let _ = writeln!( out, " -D DICT Use DICT as the dictionary for compression or decompression." ); let _ = writeln!(out); let _ = writeln!( out, " -f, --force Disable input and output checks. Allows overwriting existing files," ); let _ = writeln!( out, " receiving input from the console, printing output to STDOUT, and" ); let _ = writeln!( out, " operating on links, block devices, etc. Unrecognized formats will be" ); let _ = writeln!( out, " passed-through through as-is." ); let _ = writeln!(out); let _ = writeln!( out, " -h Display short usage and exit." ); let _ = writeln!( out, " -H, --help Display full help and exit." ); let _ = writeln!( out, " -V, --version Display the program version and exit." ); let _ = writeln!(out); } fn write_advanced_usage(out: &mut W, program_name: &str) { let version = unsafe { CStr::from_ptr(ZSTD_versionString()) }.to_string_lossy(); let _ = writeln!( out, "*** Zstandard CLI ({}-bit) v{version}, by Yann Collet ***", usize::BITS ); let _ = writeln!(out); write_basic_usage(out, program_name); let _ = writeln!(out, "\nAdvanced options:"); let _ = writeln!( out, " -c, --stdout Write to STDOUT (even if it is a console) and keep the INPUT file(s)." ); let _ = writeln!( out, " -v, --verbose Enable verbose output; pass multiple times to increase verbosity." ); let _ = writeln!( out, " -q, --quiet Suppress warnings; pass twice to suppress errors." ); let _ = writeln!( out, " --trace LOG Log tracing information to LOG." ); let _ = writeln!(out); let _ = writeln!( out, " --[no-]progress Forcibly show/hide the progress counter." ); let _ = writeln!(out); let _ = writeln!( out, " -r Operate recursively on directories." ); let _ = writeln!( out, " --filelist LIST Read a list of files to operate on from LIST." ); let _ = writeln!( out, " --output-dir-flat DIR Store processed files in DIR." ); #[cfg(unix)] let _ = writeln!( out, " --output-dir-mirror DIR Store processed files in DIR, respecting original directory structure." ); let _ = writeln!(out); let _ = writeln!( out, " -- Treat remaining arguments after `--` as files." ); let _ = writeln!(out, "\nAdvanced compression options:"); let _ = writeln!( out, " --ultra Enable levels beyond 19, up to {}; requires more memory.", max_c_level_for_ultra() ); let _ = writeln!( out, " --fast[=#] Use very fast compression levels. [Default: 1]" ); if program_name == "gzip" { let _ = writeln!( out, " --best Compatibility alias for `-9`." ); } let _ = writeln!( out, " --adapt Dynamically adapt compression level to I/O conditions." ); let _ = writeln!( out, " --long[=#] Enable long distance matching with window log #." ); let _ = writeln!(out, " --patch-from=REF Use REF as the reference point for Zstandard's diff engine."); let _ = writeln!( out, " -T# Spawn # compression threads." ); let _ = writeln!( out, " --single-thread Share a single thread for I/O and compression." ); let _ = writeln!(out, " --auto-threads={{physical|logical}}"); let _ = writeln!(out, " -B# Set job size to #."); let _ = writeln!( out, " --rsyncable Compress using a rsync-friendly method." ); let _ = writeln!( out, " --exclude-compressed Only compress files that are not already compressed." ); let _ = writeln!( out, " --stream-size=# Specify size of streaming input from STDIN." ); let _ = writeln!( out, " --size-hint=# Optimize compression parameters for streaming input." ); let _ = writeln!(out, " --target-compressed-block-size=#"); let _ = writeln!( out, " Generate compressed blocks of approximately # size." ); let _ = writeln!( out, " --no-dictID Don't write `dictID` into the header." ); let _ = writeln!( out, " --[no-]compress-literals Force (un)compressed literals." ); let _ = writeln!( out, " --[no-]row-match-finder Enable/disable the row-based matchfinder." ); let _ = writeln!( out, " --format=zstd Compress files to the `.zst` format. [Default]" ); let _ = writeln!( out, " --format=gzip Compress files to the `.gz` format." ); let _ = writeln!( out, " --format=xz Compress files to the `.xz` format." ); let _ = writeln!( out, " --format=lzma Compress files to the `.lzma` format." ); let _ = writeln!( out, " --format=lz4 Compress files to the `.lz4` format." ); let _ = writeln!(out, "\nAdvanced decompression options:"); let _ = writeln!( out, " -l Print information about compressed files." ); let _ = writeln!( out, " --test Test compressed file integrity." ); let _ = writeln!( out, " -M# Set the memory usage limit to # megabytes." ); let _ = writeln!( out, " --[no-]sparse Enable/disable sparse mode." ); let _ = writeln!( out, " --[no-]pass-through Pass through uncompressed files as-is." ); let _ = writeln!(out, "\nDictionary builder:"); let _ = writeln!( out, " --train Create a dictionary from a training set of files." ); let _ = writeln!(out, " --train-cover[=k=#,d=#,steps=#,split=#,shrink[=#]]"); let _ = writeln!( out, " --train-fastcover[=k=#,d=#,f=#,steps=#,split=#,accel=#,shrink[=#]]" ); let _ = writeln!( out, " --train-legacy[=s=#] Use the legacy algorithm." ); let _ = writeln!( out, " --maxdict=# Limit dictionary to specified size #." ); let _ = writeln!( out, " --dictID=# Force dictionary ID to #." ); let _ = writeln!(out, "\nBenchmark options:"); let _ = writeln!( out, " -b# Perform benchmarking with compression level #." ); let _ = writeln!( out, " -e# Test all compression levels up to #." ); let _ = writeln!( out, " -i# Set the minimum evaluation time to # seconds." ); } fn usage(advanced: bool) { let mut out = io::stdout().lock(); if advanced { write_advanced_usage(&mut out, "zstd"); } else { write_basic_usage(&mut out, "zstd"); } } fn print_version(quiet: bool) { let version = unsafe { CStr::from_ptr(ZSTD_versionString()) } .to_string_lossy() .into_owned(); if quiet { println!("{version}"); } else { println!( "*** Zstandard CLI ({}-bit) v{version}, by Yann Collet ***", usize::BITS ); } } fn print_cli_header(display_level: i32) { if display_level >= 3 { let version = unsafe { CStr::from_ptr(ZSTD_versionString()) }.to_string_lossy(); eprintln!( "*** Zstandard CLI ({}-bit) v{version}, by Yann Collet ***", usize::BITS ); } } fn check_lib_version() -> Result<(), String> { let expected = unsafe { CStr::from_ptr(ZSTD_rust_cli_expected_version()) }; let actual = unsafe { CStr::from_ptr(ZSTD_versionString()) }; if expected == actual { return Ok(()); } Err(format!( "incorrect library version (expecting: {}; actual: {})", expected.to_string_lossy(), actual.to_string_lossy() )) } fn parse_size(value: &str) -> Result { let split = value .find(|character: char| !character.is_ascii_digit()) .unwrap_or(value.len()); let (digits, suffix) = value.split_at(split); if digits.is_empty() { return Err( "error: only numeric values with optional suffixes K, KB, KiB, M, MB, MiB are allowed " .to_owned(), ); } let mut number = digits .parse::() .map_err(|_| "error: numeric value overflows size_t ".to_owned())?; let shift = match suffix { "" => 0, "K" | "KB" | "KiB" => 10, "M" | "MB" | "MiB" => 20, _ => return Err( "error: only numeric values with optional suffixes K, KB, KiB, M, MB, MiB are allowed " .to_owned(), ), }; number = number .checked_shl(shift) .ok_or_else(|| "error: numeric value overflows size_t ".to_owned())?; Ok(number) } fn parse_u32(value: &str, name: &str) -> Result { let _ = name; let size = parse_size(value)?; u32::try_from(size) .map_err(|_| "error: numeric value overflows 32-bit unsigned int ".to_owned()) } fn parse_i32(value: &str, name: &str) -> Result { value .parse::() .map_err(|_| format!("invalid {name}: {value:?}")) } fn parse_compression_level(value: &str) -> Result { /* The C CLI reads short-form levels as U32 before storing them in its * signed level field. Preserve that conversion for values such as * 4294967295, which represents -1 on the supported two's-complement * targets and is a valid fast-compression level. */ Ok(parse_u32(value, "compression level")? as i32) } fn parse_worker_count(value: &str) -> Result { let workers = parse_i32(value, "thread count")?; if workers < 0 { return Err(format!("thread count must not be negative: {value:?}")); } Ok(workers) } fn next_value( attached: Option<&str>, args: &[OsString], index: &mut usize, option: &str, ) -> Result { if let Some(value) = attached { if !value.is_empty() { return Ok(value.to_owned()); } } *index += 1; let Some(value) = args.get(*index) else { return Err(format!("missing argument for {option}")); }; let rendered = value.to_string_lossy().into_owned(); if rendered.starts_with('-') { return Err(format!( "{option} cannot be separated from its argument by another option" )); } Ok(rendered) } fn next_os_value(args: &[OsString], index: &mut usize, option: &str) -> Result { *index += 1; let Some(value) = args.get(*index) else { return Err(format!("missing argument for {option}")); }; if value.to_string_lossy().starts_with('-') { return Err(format!( "{option} cannot be separated from its argument by another option" )); } Ok(value.clone()) } /// Mirrors the C CLI `NEXT_FIELD` macro: an attached `=value` is used as-is /// (even when empty), otherwise the next argument is consumed. Unlike /// `next_value`, an empty attached value never falls through to the next /// argument, so `--output-dir-flat=` stays an empty (and rejected) name. fn next_field( attached: Option<&str>, args: &[OsString], index: &mut usize, option: &str, ) -> Result { if let Some(value) = attached { return Ok(value.to_owned()); } *index += 1; let Some(value) = args.get(*index) else { return Err(format!("missing argument for {option}")); }; let rendered = value.to_string_lossy().into_owned(); if rendered.starts_with('-') { return Err(format!( "{option} cannot be separated from its argument by another option" )); } Ok(rendered) } #[cfg(unix)] fn short_attached_value(value: &OsStr, start: usize) -> Option { let bytes = &value.as_bytes()[start..]; if bytes.is_empty() { return None; } let bytes = if bytes.first() == Some(&b'=') { &bytes[1..] } else { bytes }; Some(OsString::from_vec(bytes.to_vec())) } #[cfg(not(unix))] fn short_attached_value(value: &OsStr, start: usize) -> Option { let rendered = value.to_string_lossy(); let attached = &rendered[start..]; if attached.is_empty() { return None; } Some(OsString::from( attached.strip_prefix('=').unwrap_or(attached), )) } fn parse_compression_parameters(value: &str, cli: &mut Cli) -> Result<(), String> { for item in value.split(',') { let Some((name, raw)) = item.split_once('=') else { return Err(format!("invalid --zstd parameter {item:?}")); }; let parsed = parse_u32(raw, name)?; match name { "windowLog" | "wlog" => cli.compression_params.windowLog = parsed, "chainLog" | "clog" => cli.compression_params.chainLog = parsed, "hashLog" | "hlog" => cli.compression_params.hashLog = parsed, "searchLog" | "slog" => cli.compression_params.searchLog = parsed, "minMatch" | "mml" => cli.compression_params.minMatch = parsed, "targetLength" | "tlen" => cli.compression_params.targetLength = parsed, "strategy" | "strat" => cli.compression_params.strategy = parsed as c_int, "overlapLog" | "ovlog" => cli.overlap_log = Some(parsed as i32), "ldmHashLog" | "lhlog" => cli.ldm_hash_log = Some(parsed as i32), "ldmMinMatch" | "lmml" => cli.ldm_min_match = Some(parsed as i32), "ldmBucketSizeLog" | "lblog" => cli.ldm_bucket_size_log = Some(parsed as i32), "ldmHashRateLog" | "lhrlog" => cli.ldm_hash_rate_log = Some(parsed as i32), _ => return Err(format!("unknown --zstd parameter {name:?}")), } } Ok(()) } fn parse_adapt(value: &str, cli: &mut Cli) -> Result<(), String> { cli.adapt = true; if value.is_empty() { return Err("invalid --adapt parameter".to_owned()); } for item in value.split(',') { let Some((name, raw)) = item.split_once('=') else { return Err(format!("invalid --adapt parameter {item:?}")); }; match name { "min" => cli.adapt_min = Some(parse_i32(raw, "adapt minimum")?), "max" => cli.adapt_max = Some(parse_i32(raw, "adapt maximum")?), _ => return Err(format!("unknown --adapt parameter {name:?}")), } } if let (Some(minimum), Some(maximum)) = (cli.adapt_min, cli.adapt_max) { if minimum > maximum { return Err("--adapt minimum must not exceed its maximum".to_owned()); } } Ok(()) } fn default_fast_cover_params() -> ZDICT_fastCover_params_t { ZDICT_fastCover_params_t { d: 8, f: 20, steps: 4, splitPoint: 0.75, accel: DEFAULT_FASTCOVER_ACCEL, shrinkDictMaxRegression: DEFAULT_SHRINK_DICT_REGRESSION, ..ZDICT_fastCover_params_t::default() } } fn parse_training_u32(value: &str, option: &str, field: &str) -> Result { parse_u32(value, &format!("{option} {field}")) } fn parse_cover_parameters(value: &str) -> Result { let mut params = ZDICT_cover_params_t::default(); for item in value.split(',') { if item == "shrink" { params.shrinkDict = 1; params.shrinkDictMaxRegression = DEFAULT_SHRINK_DICT_REGRESSION; continue; } if let Some(raw) = item.strip_prefix("shrink=") { params.shrinkDict = 1; params.shrinkDictMaxRegression = parse_training_u32(raw, "--train-cover", "shrink regression")?; continue; } let Some((field, raw)) = item.split_once('=') else { return Err(format!("invalid --train-cover parameter {item:?}")); }; let parsed = parse_training_u32(raw, "--train-cover", field)?; match field { "k" => params.k = parsed, "d" => params.d = parsed, "steps" => params.steps = parsed, "split" => params.splitPoint = f64::from(parsed) / 100.0, _ => return Err(format!("unknown --train-cover parameter {field:?}")), } } Ok(params) } fn parse_fast_cover_parameters(value: &str) -> Result { let mut params = ZDICT_fastCover_params_t::default(); for item in value.split(',') { if item == "shrink" { params.shrinkDict = 1; params.shrinkDictMaxRegression = DEFAULT_SHRINK_DICT_REGRESSION; continue; } if let Some(raw) = item.strip_prefix("shrink=") { params.shrinkDict = 1; params.shrinkDictMaxRegression = parse_training_u32(raw, "--train-fastcover", "shrink regression")?; continue; } let Some((field, raw)) = item.split_once('=') else { return Err(format!("invalid --train-fastcover parameter {item:?}")); }; let parsed = parse_training_u32(raw, "--train-fastcover", field)?; match field { "k" => params.k = parsed, "d" => params.d = parsed, "f" => params.f = parsed, "steps" => params.steps = parsed, "split" => params.splitPoint = f64::from(parsed) / 100.0, "accel" => params.accel = parsed, _ => return Err(format!("unknown --train-fastcover parameter {field:?}")), } } Ok(params) } fn parse_legacy_parameters(value: &str) -> Result { let Some(raw) = value .strip_prefix("s=") .or_else(|| value.strip_prefix("selectivity=")) else { return Err(format!("invalid --train-legacy parameter {value:?}")); }; parse_training_u32(raw, "--train-legacy", "selectivity") } fn select_training(cli: &mut Cli, algorithm: TrainingAlgorithm) -> Result<(), String> { cli.operation = Operation::Train; cli.training_algorithm = algorithm; if cli.output.is_none() { cli.output = Some(cstring(DEFAULT_DICT_NAME)?); } Ok(()) } fn unsupported(option: &str) -> Result<(), String> { Err(format!( "{option} is not yet implemented by the Rust CLI frontend" )) } fn set_max_compression(cli: &mut Cli) -> Result<(), String> { if cfg!(target_pointer_width = "32") { return Err("--max is incompatible with 32-bit mode".to_owned()); } cli.ultra = true; cli.ldm = true; cli.compression_params = ZSTD_compressionParameters { windowLog: MAX_WINDOW_LOG, chainLog: MAX_CHAIN_LOG, hashLog: MAX_HASH_LOG, searchLog: MAX_SEARCH_LOG, minMatch: MAX_MIN_MATCH, targetLength: MAX_TARGET_LENGTH, strategy: MAX_STRATEGY, }; cli.overlap_log = Some(MAX_OVERLAP_LOG); cli.ldm_hash_log = Some(MAX_LDM_HASH_LOG); cli.ldm_hash_rate_log = Some(0); cli.ldm_min_match = Some(MAX_LDM_MIN_MATCH); cli.ldm_bucket_size_log = Some(MAX_LDM_BUCKET_SIZE_LOG); Ok(()) } fn parse_long_option( option: &str, args: &[OsString], index: &mut usize, cli: &mut Cli, ) -> Result, String> { let (name, attached) = option .split_once('=') .map_or((option, None), |(name, value)| (name, Some(value))); if attached.is_some() && matches!( name, "--compress" | "--decompress" | "--uncompress" | "--test" | "--force" | "--keep" | "--rm" | "--stdout" | "--version" | "--help" | "--verbose" | "--quiet" | "--check" | "--no-check" | "--sparse" | "--no-sparse" | "--pass-through" | "--no-pass-through" | "--content-size" | "--no-content-size" | "--no-dictID" | "--asyncio" | "--no-asyncio" | "--mmap-dict" | "--no-mmap-dict" | "--progress" | "--no-progress" | "--ultra" | "--no-row-match-finder" | "--row-match-finder" | "--rsyncable" | "--single-thread" | "--compress-literals" | "--no-compress-literals" | "--exclude-compressed" | "--no-name" | "--best" | "--list" | "--show-default-cparams" | "--train" | "--max" | "--fake-stderr-is-console" | "--trace-file-stat" ) { return Err(format!("{name} does not take an argument")); } match name { "--" => Ok(None), "--compress" => { cli.operation = Operation::Compress; Ok(None) } "--decompress" | "--uncompress" => { cli.bench_decode = true; if cli.operation != Operation::Bench { cli.operation = Operation::Decompress; } Ok(None) } "--test" => { cli.operation = Operation::Test; Ok(None) } "--force" => { cli.force = true; Ok(None) } "--keep" => { cli.remove_source = false; Ok(None) } "--no-name" => Ok(None), "--best" if cli.gzip_compat => { cli.level = 9; Ok(None) } "--rm" => { cli.remove_source = true; Ok(None) } "--stdout" => { cli.output = Some(cstring(STDOUT_MARK)?); cli.force_stdout = true; Ok(None) } "--version" => Ok(Some(Action::Version { quiet: cli.display_level < 2, })), "--help" => Ok(Some(Action::Help { advanced: true })), "--verbose" => { cli.display_level += 1; Ok(None) } "--quiet" => { cli.display_level -= 1; Ok(None) } "--check" => { cli.checksum = Some(2); Ok(None) } "--no-check" => { cli.checksum = Some(0); Ok(None) } "--sparse" => { cli.sparse = Some(2); Ok(None) } "--no-sparse" => { cli.sparse = Some(0); Ok(None) } "--pass-through" => { cli.pass_through = Some(1); Ok(None) } "--no-pass-through" => { cli.pass_through = Some(0); Ok(None) } "--content-size" => { cli.content_size = 1; Ok(None) } "--no-content-size" => { cli.content_size = 0; Ok(None) } "--no-dictID" => { cli.dict_id = Some(0); Ok(None) } "--asyncio" => { cli.async_io = Some(1); Ok(None) } "--no-asyncio" => { cli.async_io = Some(0); Ok(None) } "--mmap-dict" => { cli.mmap_dict = ZSTD_PS_ENABLE; Ok(None) } "--no-mmap-dict" => { cli.mmap_dict = ZSTD_PS_DISABLE; Ok(None) } "--progress" => { cli.progress = FIO_PS_ALWAYS; Ok(None) } "--no-progress" => { cli.progress = FIO_PS_NEVER; Ok(None) } "--ultra" => { cli.ultra = true; Ok(None) } "--fast" => { let mut level = match attached { Some(value) => parse_u32(value, "fast level")?, None => 1, }; if level == 0 { return Err("fast level must be positive".to_owned()); } level = level.min(MAX_FAST_ACCELERATION as u32); cli.level = -(level as i32); Ok(None) } "--long" => { cli.ldm = true; cli.ultra = true; if let Some(value) = attached { cli.compression_params.windowLog = parse_u32(value, "long window log")?; } else if cli.compression_params.windowLog == 0 { cli.compression_params.windowLog = DEFAULT_LONG_WINDOW_LOG; } Ok(None) } "--adapt" => { if let Some(value) = attached { parse_adapt(value, cli)?; } else { cli.adapt = true; } Ok(None) } "--no-row-match-finder" => { cli.row_match_finder = ZSTD_PS_DISABLE; Ok(None) } "--row-match-finder" => { cli.row_match_finder = ZSTD_PS_ENABLE; Ok(None) } "--rsyncable" => { cli.rsyncable = true; Ok(None) } "--compress-literals" => { cli.literal_compression = Some(ZSTD_PS_ENABLE); Ok(None) } "--no-compress-literals" => { cli.literal_compression = Some(ZSTD_PS_DISABLE); Ok(None) } "--exclude-compressed" => { cli.exclude_compressed = true; Ok(None) } "--threads" => { let value = next_value(attached, args, index, "--threads")?; cli.workers = Some(parse_worker_count(&value)?); Ok(None) } "--auto-threads" => { let value = next_field(attached, args, index, name)?; cli.auto_threads_logical = match value.as_str() { "physical" => false, "logical" => true, _ => return Err(format!("unknown auto-thread mode {value:?}")), }; Ok(None) } "--single-thread" => { /* As in the C CLI: zero workers plus a latch that suppresses the * automatic core-count resolution, so fileio runs its * single-thread streaming mode (slightly different from -T1). */ cli.workers = Some(0); cli.single_thread = true; Ok(None) } "--memlimit" | "--memory" | "--memlimit-decompress" => { let value = next_value(attached, args, index, name)?; cli.mem_limit = Some(parse_u32(&value, "memory limit")?); Ok(None) } "--block-size" => { let value = next_value(attached, args, index, name)?; cli.block_size = Some(parse_size(&value)?); Ok(None) } "--stream-size" => { let value = next_value(attached, args, index, name)?; cli.stream_src_size = Some(parse_size(&value)?); Ok(None) } "--target-compressed-block-size" => { let value = next_value(attached, args, index, name)?; cli.target_cblock_size = Some(parse_size(&value)?); Ok(None) } "--size-hint" => { let value = next_value(attached, args, index, name)?; cli.src_size_hint = Some(parse_size(&value)?); Ok(None) } "--zstd" => { let value = next_value(attached, args, index, "--zstd")?; parse_compression_parameters(&value, cli)?; Ok(None) } "--list" => { cli.operation = Operation::List; Ok(None) } "--show-default-cparams" => { cli.show_default_cparams = true; Ok(None) } "--train" => { select_training(cli, cli.training_algorithm)?; Ok(None) } "--train-cover" => { let params = attached.map_or_else( || Ok(ZDICT_cover_params_t::default()), parse_cover_parameters, )?; select_training(cli, TrainingAlgorithm::Cover)?; cli.cover_params = params; Ok(None) } "--train-fastcover" => { let params = attached.map_or_else( || Ok(ZDICT_fastCover_params_t::default()), parse_fast_cover_parameters, )?; select_training(cli, TrainingAlgorithm::FastCover)?; cli.fast_cover_params = params; Ok(None) } "--train-legacy" => { let selectivity = attached.map_or_else(|| Ok(cli.dictionary_selectivity), parse_legacy_parameters)?; select_training(cli, TrainingAlgorithm::Legacy)?; cli.dictionary_selectivity = selectivity; Ok(None) } "--maxdict" => { let value = next_field(attached, args, index, name)?; cli.max_dict_size = parse_u32(&value, "maximum dictionary size")? as usize; Ok(None) } "--dictID" => { let value = next_field(attached, args, index, name)?; cli.dictionary_id = parse_u32(&value, "dictionary ID")?; Ok(None) } "--format" => { let value = next_field(attached, args, index, name)?; cli.compression_format = CompressionFormat::parse(&value)?; Ok(None) } "--trace" => { let value = next_field(attached, args, index, name)?; cli.trace = Some(cstring(&value)?); Ok(None) } "--filelist" => { let value = next_field(attached, args, index, name)?; cli.file_lists.push(cstring(&value)?); Ok(None) } "--output-dir-flat" => { let value = next_field(attached, args, index, name)?; if value.is_empty() { return Err( "output dir cannot be empty string (did you mean to pass '.' instead?)" .to_owned(), ); } cli.output_dir_flat = Some(cstring(&value)?); Ok(None) } #[cfg(unix)] "--output-dir-mirror" => { /* Parsed only where the C CLI defines UTIL_HAS_MIRRORFILELIST * (POSIX); elsewhere the option falls through as unknown. */ let value = next_field(attached, args, index, name)?; if value.is_empty() { return Err( "output dir cannot be empty string (did you mean to pass '.' instead?)" .to_owned(), ); } cli.output_dir_mirror = Some(cstring(&value)?); Ok(None) } "--max" => { set_max_compression(cli)?; Ok(None) } "--fake-stderr-is-console" => { cli.fake_stderr_is_console = true; Ok(None) } "--trace-file-stat" => { cli.trace_file_stat = true; Ok(None) } "--patch-from" => { if cli.dictionary.is_some() { return Err("can't use -D and --patch-from=# at the same time".to_owned()); } let value = next_field(attached, args, index, name)?; cli.patch_from = Some(cstring(&value)?); cli.ultra = true; Ok(None) } "--priority" | "--fake-stdin-is-console" | "--fake-stdout-is-console" => { unsupported(name)?; Ok(None) } _ => Err(format!("unknown option {option}")), } } fn parse_short_options( value: &str, raw_value: &OsStr, args: &[OsString], index: &mut usize, cli: &mut Cli, ) -> Result, String> { let mut offset = 1usize; let bytes = value.as_bytes(); while offset < bytes.len() { let option = bytes[offset] as char; match option { '0'..='9' => { let mut digits_end = offset; while digits_end < bytes.len() && bytes[digits_end].is_ascii_digit() { digits_end += 1; } cli.level = parse_compression_level(&value[offset..digits_end])?; offset = digits_end; continue; } 'd' => { cli.bench_decode = true; if cli.operation != Operation::Bench { cli.operation = Operation::Decompress; } } 'z' => cli.operation = Operation::Compress, 't' => cli.operation = Operation::Test, 'b' => cli.operation = Operation::Bench, 'l' => cli.operation = Operation::List, #[cfg(any(unix, windows))] 'r' => cli.recursive = true, 'e' | 'i' => { // Benchmark range end (-e#) and duration (-i#): like the C // parser, digits attach directly and default to 0. let mut digits_end = offset + 1; while digits_end < bytes.len() && bytes[digits_end].is_ascii_digit() { digits_end += 1; } let digits = &value[offset + 1..digits_end]; if option == 'e' { cli.bench_end_level = Some(if digits.is_empty() { 0 } else { parse_i32(digits, "benchmark end level")? }); } else { cli.bench_nb_seconds = Some(if digits.is_empty() { 0 } else { digits .parse::() .map_err(|_| format!("invalid benchmark duration: {digits:?}"))? }); } offset = digits_end; continue; } 'c' => { cli.output = Some(cstring(STDOUT_MARK)?); cli.force_stdout = true; } 'f' => cli.force = true, 'k' => cli.remove_source = false, 'n' => {} 'q' => cli.display_level -= 1, 'v' => cli.display_level += 1, 'C' => cli.checksum = Some(2), 'h' => return Ok(Some(Action::Help { advanced: false })), 'H' => return Ok(Some(Action::Help { advanced: true })), 'V' => { return Ok(Some(Action::Version { quiet: cli.display_level < 2, })) } 'o' | 'D' | 'T' | 'M' | 'B' => { let attached = short_attached_value(raw_value, offset + 1); let argument = attached .map(Ok) .unwrap_or_else(|| next_os_value(args, index, &format!("-{option}")))?; match option { 'o' => cli.output = Some(os_cstring(&argument)?), 'D' => { if cli.patch_from.is_some() { return Err( "can't use -D and --patch-from=# at the same time".to_owned() ); } cli.dictionary = Some(os_cstring(&argument)?); } 'T' => cli.workers = Some(parse_worker_count(&argument.to_string_lossy())?), 'M' => { cli.mem_limit = Some(parse_u32(&argument.to_string_lossy(), "memory limit")?) } 'B' => cli.block_size = Some(parse_size(&argument.to_string_lossy())?), _ => unreachable!(), } break; } 's' => { let mut digits_end = offset + 1; while digits_end < bytes.len() && bytes[digits_end].is_ascii_digit() { digits_end += 1; } if digits_end == offset + 1 { return Err("missing argument for -s".to_owned()); } cli.dictionary_selectivity = parse_u32(&value[offset + 1..digits_end], "dictionary selectivity")?; offset = digits_end; continue; } 'p' | 'P' | 'S' => { unsupported(&format!("-{option}"))?; } _ => return Err(format!("unknown option -{option}")), } offset += 1; } Ok(None) } #[derive(Debug)] struct ParseError { message: String, #[allow(dead_code)] details: String, show_usage: bool, } impl ParseError { fn direct(message: String) -> Self { Self::direct_with_details(message.clone(), message) } fn direct_with_details(message: String, details: String) -> Self { Self { message, details, show_usage: false, } } fn incorrect_parameter(parameter: &str, details: String, show_usage: bool) -> Self { Self { message: format!("Incorrect parameter: {parameter}"), details, show_usage, } } #[cfg(test)] fn contains(&self, needle: &str) -> bool { self.message.contains(needle) || self.details.contains(needle) } } fn classify_parse_error(error: String, parameter: &str, display_level: i32) -> ParseError { if error.starts_with("error:") { return ParseError::direct(error); } if error.starts_with("output dir cannot be empty string") { return ParseError::direct(format!("error: {error}")); } if error.starts_with("missing argument for ") { return ParseError::direct_with_details( "error: missing command argument ".to_owned(), error, ); } if error.contains("cannot be separated from its argument") { return ParseError::direct_with_details( "error: command cannot be separated from its argument by another command ".to_owned(), error, ); } if let Some(option) = error.strip_prefix("unknown option ") { return ParseError::incorrect_parameter(option, error.clone(), display_level >= 2); } ParseError::incorrect_parameter(parameter, error, display_level >= 2) } fn parse_args(args: Vec) -> Result { let program_name = args .first() .map_or_else(|| "zstd".to_owned(), |value| program_basename(value)); let mut cli = Cli::new(&program_name); let mut end_of_options = false; let mut index = 1usize; while index < args.len() { let rendered = args[index].to_string_lossy().into_owned(); if end_of_options { cli.inputs .push(os_cstring(&args[index]).map_err(ParseError::direct)?); } else if rendered == "--" { end_of_options = true; } else if rendered == "-" { cli.inputs .push(cstring(STDIN_MARK).map_err(ParseError::direct)?); } else if rendered.starts_with("--") { let action = parse_long_option(&rendered, &args, &mut index, &mut cli) .map_err(|error| classify_parse_error(error, &rendered, cli.display_level))?; if let Some(action) = action { return Ok(action); } } else if rendered.starts_with('-') { let action = parse_short_options(&rendered, &args[index], &args, &mut index, &mut cli) .map_err(|error| classify_parse_error(error, &rendered, cli.display_level))?; if let Some(action) = action { return Ok(action); } } else { cli.inputs .push(os_cstring(&args[index]).map_err(ParseError::direct)?); } index += 1; } Ok(Action::Run(Box::new(cli))) } unsafe fn apply_preferences(cli: &Cli, prefs: *mut FIO_prefs_t, ctx: *mut FIO_ctx_t) { let display_level = if is_stdout(cli.output.as_ref()) && cli.display_level == 2 { 1 } else { cli.display_level }; unsafe { g_utilDisplayLevel = display_level; FIO_setCompressionType(prefs, cli.compression_format.fio_type()); FIO_setNotificationLevel(display_level); FIO_setProgressSetting( if !cli.fake_stderr_is_console && !io::stderr().is_terminal() && cli.progress != FIO_PS_ALWAYS { FIO_PS_NEVER } else { cli.progress }, ); FIO_setRemoveSrcFile( prefs, i32::from( cli.remove_source && cli.operation != Operation::Test && !is_stdout(cli.output.as_ref()), ), ); FIO_setAllowBlockDevices(prefs, i32::from(cli.force)); FIO_setMMapDict(prefs, cli.mmap_dict); FIO_setUseRowMatchFinder(prefs, cli.row_match_finder); FIO_setMemLimit( prefs, cli.mem_limit .filter(|limit| *limit != 0) .unwrap_or_else(|| { if cli.compression_params.windowLog == 0 { DEFAULT_MEM_LIMIT } else { 1_u32 << (cli.compression_params.windowLog & 31) } }), ); #[cfg(feature = "compression")] FIO_setNbWorkers( prefs, resolved_worker_count(cli.workers, cli.single_thread, cli.auto_threads_logical), ); FIO_setLdmFlag(prefs, u32::from(cli.ldm)); FIO_setAdaptiveMode(prefs, i32::from(cli.adapt)); FIO_setRsyncable(prefs, i32::from(cli.rsyncable)); FIO_setExcludeCompressedFile(prefs, i32::from(cli.exclude_compressed)); if cli.force { FIO_overwriteMode(prefs); } if let Some(value) = cli.checksum { FIO_setChecksumFlag(prefs, value); } if cli.operation == Operation::Compress { FIO_setSparseWrite(prefs, 0); } else if let Some(value) = cli.sparse { FIO_setSparseWrite(prefs, value); } if let Some(value) = cli.pass_through { FIO_setPassThroughFlag(prefs, value); } FIO_setPatchFromMode(prefs, i32::from(cli.patch_from.is_some())); FIO_setContentSize(prefs, cli.content_size); if let Some(value) = cli.dict_id { FIO_setDictIDFlag(prefs, value); } if let Some(value) = cli.async_io { FIO_setAsyncIOFlag(prefs, value); } if let Some(value) = cli.block_size { FIO_setBlockSize(prefs, value as c_int); } if let Some(value) = cli.ldm_hash_log { FIO_setLdmHashLog(prefs, value); } if let Some(value) = cli.ldm_min_match { FIO_setLdmMinMatch(prefs, value); } if let Some(value) = cli.ldm_bucket_size_log { FIO_setLdmBucketSizeLog(prefs, value); } if let Some(value) = cli.ldm_hash_rate_log { FIO_setLdmHashRateLog(prefs, value); } if let Some(value) = cli.overlap_log { FIO_setOverlapLog(prefs, value); } #[cfg(feature = "compression")] { FIO_setAdaptMin(prefs, cli.adapt_min.unwrap_or_else(|| ZSTD_minCLevel())); FIO_setAdaptMax(prefs, cli.adapt_max.unwrap_or_else(|| ZSTD_maxCLevel())); } if let Some(value) = cli.stream_src_size { FIO_setStreamSrcSize(prefs, value); } if let Some(value) = cli.target_cblock_size { FIO_setTargetCBlockSize(prefs, value); } if let Some(value) = cli.src_size_hint { FIO_setSrcSizeHint(prefs, value); } if let Some(value) = cli.literal_compression { FIO_setLiteralCompressionMode(prefs, value); } FIO_setNbFilesTotal(ctx, cli.inputs.len() as c_int); FIO_setHasStdinInput(ctx, i32::from(cli.inputs.iter().any(is_stdin))); FIO_setHasStdoutOutput(ctx, i32::from(is_stdout(cli.output.as_ref()))); } } fn is_stdout(value: Option<&CString>) -> bool { value.is_some_and(|value| value.as_bytes() == STDOUT_MARK.as_bytes()) } fn is_stdin(value: &CString) -> bool { value.as_bytes() == STDIN_MARK.as_bytes() } fn fileio_dictionary(cli: &Cli) -> Option<&CString> { cli.patch_from.as_ref().or(cli.dictionary.as_ref()) } #[cfg(unix)] fn is_non_fifo_symlink(input: &CString) -> bool { if unsafe { UTIL_isLink(input.as_ptr()) } == 0 { return false; } let path = Path::new(OsStr::from_bytes(input.as_bytes())); let Ok(metadata) = fs::symlink_metadata(path) else { return false; }; metadata.file_type().is_symlink() && !fs::metadata(path).is_ok_and(|target| target.file_type().is_fifo()) } #[cfg(not(unix))] fn is_non_fifo_symlink(input: &CString) -> bool { if unsafe { UTIL_isLink(input.as_ptr()) } == 0 { return false; } let path = Path::new(&input.to_string_lossy().into_owned()); fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) } fn filter_symlink_inputs(cli: &mut Cli) { if cli.force { return; } cli.inputs.retain(|input| { if is_stdin(input) || !is_non_fifo_symlink(input) { return true; } if cli.display_level >= 2 { eprintln!( "zstd: Warning : {} is a symbolic link, ignoring", input.to_string_lossy() ); } false }); } fn check_terminal_safety(cli: &Cli) -> Result<(), String> { let has_stdin = cli.inputs.iter().any(is_stdin); let stdin_is_console = if has_stdin { unsafe { UTIL_isConsole(stdin) != 0 } } else { false }; let output_is_stdout = is_stdout(cli.output.as_ref()); let stdout_is_console = if output_is_stdout || (cli.operation == Operation::Decompress && !has_stdin) { unsafe { UTIL_isConsole(stdout) != 0 } } else { false }; let stderr_is_console = unsafe { UTIL_isConsole(stderr) != 0 }; if has_stdin && !cli.force && stdin_is_console { return Err("stdin is a console, aborting".to_owned()); } if has_stdin && output_is_stdout && !cli.force && !cli.force_stdout && cli.operation != Operation::Decompress && stdout_is_console { return Err("stdout is a console, aborting".to_owned()); } let _ = stderr_is_console; Ok(()) } #[cfg(feature = "compression")] unsafe fn print_default_cparams(cli: &Cli) { let dict_size = fileio_dictionary(cli).map_or(0, |dictionary| unsafe { UTIL_getFileSize(dictionary.as_ptr()) as usize }); for input in &cli.inputs { let file_size = unsafe { UTIL_getFileSize(input.as_ptr()) }; let cparams = unsafe { ZSTD_getCParams(cli.level, file_size, dict_size) }; let strategy = ZSTD_STRATEGY_NAMES .get(cparams.strategy as usize) .copied() .unwrap_or(""); if file_size != u64::MAX { eprintln!("{} ({} bytes)", input.to_string_lossy(), file_size); } else { eprintln!("{} (src size unknown)", input.to_string_lossy()); } eprintln!(" - windowLog : {}", cparams.windowLog); eprintln!(" - chainLog : {}", cparams.chainLog); eprintln!(" - hashLog : {}", cparams.hashLog); eprintln!(" - searchLog : {}", cparams.searchLog); eprintln!(" - minMatch : {}", cparams.minMatch); eprintln!(" - targetLength : {}", cparams.targetLength); eprintln!(" - strategy : {strategy} ({})", cparams.strategy); } } #[cfg(feature = "compression")] unsafe fn print_actual_cparams(cli: &Cli) { let dict_size = fileio_dictionary(cli).map_or(0, |dictionary| unsafe { UTIL_getFileSize(dictionary.as_ptr()) as usize }); for input in &cli.inputs { let file_size = unsafe { UTIL_getFileSize(input.as_ptr()) }; let mut actual = unsafe { ZSTD_getCParams(cli.level, file_size, dict_size) }; let requested = cli.compression_params; if requested.windowLog != 0 { actual.windowLog = requested.windowLog; } if requested.chainLog != 0 { actual.chainLog = requested.chainLog; } if requested.hashLog != 0 { actual.hashLog = requested.hashLog; } if requested.searchLog != 0 { actual.searchLog = requested.searchLog; } if requested.minMatch != 0 { actual.minMatch = requested.minMatch; } if requested.targetLength != 0 { actual.targetLength = requested.targetLength; } if requested.strategy != 0 { actual.strategy = requested.strategy; } eprintln!( "--zstd=wlog={},clog={},hlog={},slog={},mml={},tlen={},strat={}", actual.windowLog, actual.chainLog, actual.hashLog, actual.searchLog, actual.minMatch, actual.targetLength, actual.strategy ); } } #[cfg(feature = "compression")] unsafe fn run_compress( cli: &Cli, ctx: *mut FIO_ctx_t, prefs: *mut FIO_prefs_t, inputs: &[*const c_char], output: *const c_char, dictionary: *const c_char, ) -> c_int { if inputs.len() == 1 && !output.is_null() { unsafe { FIO_compressFilename( ctx, prefs, output, inputs[0], dictionary, cli.level, cli.compression_params, ) } } else { unsafe { FIO_compressMultipleFilenames( ctx, prefs, inputs.as_ptr(), output_dir_mirror(cli), output_dir_flat(cli), output, cli.compression_format.suffix().as_ptr().cast(), dictionary, cli.level, cli.compression_params, ) } } } fn output_dir_flat(cli: &Cli) -> *const c_char { cli.output_dir_flat .as_ref() .map_or(ptr::null(), |value| value.as_ptr()) } fn output_dir_mirror(cli: &Cli) -> *const c_char { cli.output_dir_mirror .as_ref() .map_or(ptr::null(), |value| value.as_ptr()) } #[cfg(feature = "decompression")] unsafe fn run_decompress( cli: &Cli, ctx: *mut FIO_ctx_t, prefs: *mut FIO_prefs_t, inputs: &[*const c_char], output: *const c_char, dictionary: *const c_char, ) -> c_int { match cli.operation { Operation::Test => { let null_output = cstring(NULL_MARK).expect("static null marker"); unsafe { FIO_setTestMode(prefs, 1); FIO_decompressMultipleFilenames( ctx, prefs, inputs.as_ptr(), output_dir_mirror(cli), output_dir_flat(cli), null_output.as_ptr(), dictionary, ) } } Operation::Decompress if inputs.len() == 1 && !output.is_null() => unsafe { FIO_decompressFilename(ctx, prefs, output, inputs[0], dictionary) }, Operation::Decompress => unsafe { FIO_decompressMultipleFilenames( ctx, prefs, inputs.as_ptr(), output_dir_mirror(cli), output_dir_flat(cli), output, dictionary, ) }, Operation::Compress | Operation::Bench | Operation::List | Operation::Train => { unreachable!("compression, benchmark, and list are dispatched separately") } } } /// Copies every name out of a C `FileNamesTable` and releases the table. unsafe fn drain_file_names_table(table: *mut FileNamesTable, into: &mut Vec) { unsafe { for entry in 0..(*table).tableSize { let name = *(*table).fileNames.add(entry); if !name.is_null() { into.push(CStr::from_ptr(name).to_owned()); } } UTIL_freeFileNamesTable(table); } } /// Appends the names read from every `--filelist` argument to the inputs. /// As in the C CLI, a list that cannot be read (missing, irregular, empty, /// or with over-long lines) aborts the whole run. fn expand_file_lists(cli: &mut Cli) -> Result<(), String> { let file_lists = std::mem::take(&mut cli.file_lists); for list in &file_lists { let table = unsafe { UTIL_createFileNamesTable_fromFileName(list.as_ptr()) }; if table.is_null() { return Err(format!("error reading {}", list.to_string_lossy())); } unsafe { drain_file_names_table(table, &mut cli.inputs) }; } Ok(()) } /// Replaces the inputs with their recursive directory expansion. Symbolic /// links are followed only under --force, matching the C `followLinks` flag. fn expand_recursive_inputs(cli: &mut Cli) -> Result<(), String> { let pointers: Vec<*const c_char> = cli.inputs.iter().map(|value| value.as_ptr()).collect(); let names = if pointers.is_empty() { ptr::null() } else { pointers.as_ptr() }; let table = unsafe { UTIL_createExpandedFNT(names, pointers.len(), c_int::from(cli.force)) }; if table.is_null() { /* The C CLI treats this allocation failure as fatal (CONTROL). */ return Err("recursive directory expansion failed".to_owned()); } let mut expanded = Vec::new(); unsafe { drain_file_names_table(table, &mut expanded) }; cli.inputs = expanded; Ok(()) } /// Prints frame information for every input through `FIO_listMultipleFiles`, /// which owns the display-level gating, the stdin refusal, and the totals. #[cfg(feature = "decompression")] fn run_list(cli: &Cli) -> i32 { let inputs: Vec<*const c_char> = cli.inputs.iter().map(|value| value.as_ptr()).collect(); unsafe { FIO_listMultipleFiles(inputs.len() as c_uint, inputs.as_ptr(), cli.display_level) } } #[cfg(feature = "benchmark")] fn normalize_benchmark_levels( start_c_level: c_int, end_c_level: c_int, mode: c_int, max_c_level: c_int, ) -> (c_int, c_int) { let mut start_level = start_c_level; let mut end_level = end_c_level; if mode == crate::benchzstd::BMK_decodeOnly { start_level = 0; end_level = 0; } if start_level > max_c_level { start_level = max_c_level; } if end_level > max_c_level { end_level = max_c_level; } if end_level < start_level { end_level = start_level; } (start_level, end_level) } /// Benchmark bridge formerly implemented by `programs/zstdcli.c`. /// /// Keep the exported name because it is a useful program-side ABI boundary, /// but make the Rust CLI archive own both the normalization and dispatch. #[cfg(feature = "benchmark")] #[no_mangle] pub unsafe extern "C" fn ZSTD_rust_cli_bench( file_names: *const *const c_char, nb_files: c_uint, dict_file_name: *const c_char, start_c_level: c_int, end_c_level: c_int, compression_params: *const ZSTD_compressionParameters, display_level: c_int, nb_seconds: c_uint, block_size: usize, nb_workers: c_int, mode: c_int, ) -> c_int { let mut advanced_params = crate::benchzstd::BMK_initAdvancedParams(); advanced_params.nbSeconds = nb_seconds; advanced_params.blockSize = block_size; advanced_params.nbWorkers = nb_workers; advanced_params.mode = mode; let (start_level, end_level) = normalize_benchmark_levels(start_c_level, end_c_level, mode, unsafe { ZSTD_maxCLevel() }); let compression_params = compression_params.cast(); if nb_files == 0 { /* No input file: benchmark a synthetic sample (lorem generator). */ unsafe { crate::benchzstd::BMK_syntheticTest( -1.0, start_level, end_level, compression_params, display_level, &advanced_params, ) } } else { unsafe { crate::benchzstd::BMK_benchFilesAdvanced( file_names, nb_files, dict_file_name, start_level, end_level, compression_params, display_level, &advanced_params, ) } } } /// `ZSTD_NOBENCH` compatibility bridge for compact CLI archives. #[cfg(not(feature = "benchmark"))] #[no_mangle] pub unsafe extern "C" fn ZSTD_rust_cli_bench( file_names: *const *const c_char, nb_files: c_uint, dict_file_name: *const c_char, start_c_level: c_int, end_c_level: c_int, compression_params: *const ZSTD_compressionParameters, display_level: c_int, nb_seconds: c_uint, block_size: usize, nb_workers: c_int, mode: c_int, ) -> c_int { let _ = ( file_names, nb_files, dict_file_name, start_c_level, end_c_level, compression_params, display_level, nb_seconds, block_size, nb_workers, mode, ); -1 } /// Runs benchmark mode through the Rust bridge. No input file means a /// synthetic-sample benchmark, matching the C CLI. fn run_bench(cli: &Cli) -> Result { let inputs: Vec<*const c_char> = cli.inputs.iter().map(|value| value.as_ptr()).collect(); let dictionary = cli .dictionary .as_ref() .map_or(ptr::null(), |value| value.as_ptr()); let workers = { #[cfg(feature = "compression")] { unsafe { resolved_worker_count(cli.workers, cli.single_thread, cli.auto_threads_logical) } } #[cfg(not(feature = "compression"))] { cli.workers.unwrap_or(1) } }; let result = unsafe { ZSTD_rust_cli_bench( inputs.as_ptr(), inputs.len() as c_uint, dictionary, cli.level, cli.bench_end_level.unwrap_or(cli.level), &cli.compression_params, cli.display_level, cli.bench_nb_seconds.unwrap_or(DEFAULT_BENCH_NB_SECONDS), cli.block_size.unwrap_or(0), // The C CLI benchmarks single-threaded unless -T was given. workers, if cli.bench_decode { 1 } else { 0 }, ) }; if result < 0 { return Err("benchmark mode is not available in this build".to_owned()); } Ok(result) } /// Runs dictionary training through the narrow `programs/dibio.c` ABI. The /// file loading and output path stay in that bridge for now; the selected /// dictionary builder symbols are supplied by the Rust library archive. fn run_train(cli: &Cli) -> Result { #[cfg(not(all(feature = "compression", feature = "dict-builder")))] { let _ = cli; Err("training mode not available".to_owned()) } #[cfg(all(feature = "compression", feature = "dict-builder"))] { let default_output = cstring(DEFAULT_DICT_NAME).expect("static dictionary name"); let output = cli.output.as_ref().unwrap_or(&default_output); let inputs: Vec<*const c_char> = cli.inputs.iter().map(|value| value.as_ptr()).collect(); let workers = unsafe { resolved_worker_count(cli.workers, cli.single_thread, cli.auto_threads_logical) } as c_uint; let z_params = ZDICT_params_t { compressionLevel: cli.level, notificationLevel: cli.display_level as c_uint, dictID: cli.dictionary_id, }; let chunk_size = cli.block_size.unwrap_or(0); let mem_limit = cli.mem_limit.unwrap_or(0); let result = match cli.training_algorithm { TrainingAlgorithm::Cover => { let mut params = cli.cover_params; params.nbThreads = workers; params.zParams = z_params; let optimize = c_int::from(params.k == 0 || params.d == 0); unsafe { DiB_trainFromFiles( output.as_ptr(), cli.max_dict_size, inputs.as_ptr(), inputs.len() as c_int, chunk_size, ptr::null_mut(), &mut params, ptr::null_mut(), optimize, mem_limit, ) } } TrainingAlgorithm::FastCover => { let mut params = cli.fast_cover_params; params.nbThreads = workers; params.zParams = z_params; let optimize = c_int::from(params.k == 0 || params.d == 0); unsafe { DiB_trainFromFiles( output.as_ptr(), cli.max_dict_size, inputs.as_ptr(), inputs.len() as c_int, chunk_size, ptr::null_mut(), ptr::null_mut(), &mut params, optimize, mem_limit, ) } } TrainingAlgorithm::Legacy => { let mut params = ZDICT_legacy_params_t { selectivityLevel: cli.dictionary_selectivity, zParams: z_params, }; unsafe { DiB_trainFromFiles( output.as_ptr(), cli.max_dict_size, inputs.as_ptr(), inputs.len() as c_int, chunk_size, &mut params, ptr::null_mut(), ptr::null_mut(), 0, mem_limit, ) } } }; Ok(result) } } struct TraceGuard { enabled: bool, } impl TraceGuard { fn new(filename: Option<&CString>) -> Self { if let Some(filename) = filename { unsafe { TRACE_enable(filename.as_ptr()) }; Self { enabled: true } } else { Self { enabled: false } } } } impl Drop for TraceGuard { fn drop(&mut self) { if self.enabled { unsafe { TRACE_finish() }; } } } fn run_cli(mut cli: Cli) -> Result { if cli.fake_stderr_is_console { unsafe { UTIL_fakeStderrIsConsole() }; } if cli.trace_file_stat { unsafe { UTIL_traceFileStat() }; } let _trace_guard = TraceGuard::new(cli.trace.as_ref()); unsafe { /* The C CLI publishes the display level to util.c before any table * expansion, so traversal warnings obey -q/-v. */ g_utilDisplayLevel = cli.display_level; } let explicit_input_count = cli.inputs.len(); filter_symlink_inputs(&mut cli); if explicit_input_count > 0 && cli.inputs.is_empty() { return Ok(1); } expand_file_lists(&mut cli)?; /* Names known before recursive expansion: an input set that named only * (possibly empty) directories must not fall back to stdin below. */ let input_name_count = cli.inputs.len(); if cli.recursive { expand_recursive_inputs(&mut cli)?; } if cli.operation == Operation::List { #[cfg(feature = "decompression")] return Ok(run_list(&cli)); #[cfg(not(feature = "decompression"))] return Err("file information is not supported".to_owned()); } if cli.operation == Operation::Bench { if cli.compression_format != CompressionFormat::Zstd { return Err("benchmark mode only supports the zstd format".to_owned()); } return run_bench(&cli); } if cli.operation == Operation::Train { return run_train(&cli); } if cli.operation == Operation::Test { cli.output = Some(cstring(NULL_MARK)?); cli.remove_source = false; } if cli.inputs.is_empty() { if input_name_count > 0 { if cli.display_level >= 1 { eprintln!( "please provide correct input file(s) or non-empty directories -- ignored" ); } return Ok(0); } cli.inputs.push(cstring(STDIN_MARK)?); if cli.output.is_none() { cli.output = Some(cstring(STDOUT_MARK)?); } } if cli.inputs.len() == 1 && cli.inputs[0].as_bytes() == STDIN_MARK.as_bytes() && cli.output.is_none() { cli.output = Some(cstring(STDOUT_MARK)?); } if cli.show_default_cparams && cli.operation == Operation::Decompress { return Err("error : can't use --show-default-cparams in decompression mode".to_owned()); } check_terminal_safety(&cli)?; print_cli_header(cli.display_level); #[cfg(feature = "compression")] if cli.operation == Operation::Decompress && cli.display_level >= 2 && cli.workers.is_some_and(|workers| workers > 1) { eprintln!("Warning : decompression does not support multi-threading"); } if cli.operation == Operation::Compress { #[cfg(not(feature = "compression"))] return Err("Compression not supported".to_owned()); #[cfg(feature = "compression")] { let min_level = unsafe { ZSTD_minCLevel() }; let max_level = unsafe { ZSTD_maxCLevel() }; let ceiling = if cli.ultra { max_level } else { DEFAULT_MAX_CLEVEL.min(max_level) }; if cli.level > ceiling { if cli.display_level >= 2 { eprintln!("zstd: warning: compression level reduced to {ceiling}"); } cli.level = ceiling; } if cli.level < min_level { return Err(format!( "compression level {} is below {min_level}", cli.level )); } if let (Some(minimum), Some(maximum)) = (cli.adapt_min, cli.adapt_max) { if minimum > maximum { return Err("adaptation minimum exceeds maximum".to_owned()); } cli.level = cli.level.clamp(minimum, maximum); } } } else { #[cfg(not(feature = "decompression"))] return Err("Decompression not supported".to_owned()); } if cli.patch_from.is_some() && cli.inputs.len() > 1 { return Err("can't use --patch-from=# on multiple files".to_owned()); } let prefs = unsafe { FIO_createPreferences() }; let ctx = unsafe { FIO_createContext() }; if prefs.is_null() || ctx.is_null() { unsafe { if !prefs.is_null() { FIO_freePreferences(prefs); } if !ctx.is_null() { FIO_freeContext(ctx); } } return Err("could not allocate C file-I/O state".to_owned()); } let result = { unsafe { FIO_addAbortHandler(); apply_preferences(&cli, prefs, ctx); #[cfg(feature = "compression")] if cli.operation == Operation::Compress { if cli.show_default_cparams { print_default_cparams(&cli); } if cli.display_level >= 4 { print_actual_cparams(&cli); } } } let output = cli .output .as_ref() .map_or(ptr::null(), |value| value.as_ptr()); let dictionary = fileio_dictionary(&cli).map_or(ptr::null(), |value| value.as_ptr()); let inputs: Vec<*const c_char> = cli.inputs.iter().map(|value| value.as_ptr()).collect(); match cli.operation { Operation::Compress => { #[cfg(feature = "compression")] { unsafe { run_compress(&cli, ctx, prefs, &inputs, output, dictionary) } } #[cfg(not(feature = "compression"))] unreachable!("unsupported compression was rejected above") } Operation::Decompress | Operation::Test => { #[cfg(feature = "decompression")] { unsafe { run_decompress(&cli, ctx, prefs, &inputs, output, dictionary) } } #[cfg(not(feature = "decompression"))] unreachable!("unsupported decompression was rejected above") } Operation::Bench | Operation::List | Operation::Train => { unreachable!("benchmark and list modes were dispatched earlier") } } }; unsafe { FIO_freePreferences(prefs); FIO_freeContext(ctx); } Ok(result) } fn run_from_args(args: Vec) -> c_int { let program_name = args .first() .map_or_else(|| "zstd".to_owned(), |value| program_basename(value)); match parse_args(args) { Ok(Action::Help { advanced }) => { usage(advanced); 0 } Ok(Action::Version { quiet }) => { print_version(quiet); 0 } Ok(Action::Run(cli)) => match run_cli(*cli) { Ok(result) => result, Err(error) => { eprintln!("zstd: {error}"); 1 } }, Err(error) => { eprintln!("{}", error.message); if error.show_usage { let mut out = io::stderr().lock(); write_basic_usage(&mut out, &program_name); } 1 } } } unsafe fn argv_to_os_strings( arg_count: c_int, argv: *const *const c_char, ) -> Result, String> { if arg_count <= 0 || argv.is_null() { return Err("invalid argv supplied by C main".to_owned()); } let count = arg_count as usize; let mut args = Vec::with_capacity(count); for index in 0..count { let argument = unsafe { *argv.add(index) }; if argument.is_null() { return Err(format!("argv[{index}] is null")); } let bytes = unsafe { CStr::from_ptr(argument) }.to_bytes(); #[cfg(unix)] args.push(OsString::from_vec(bytes.to_vec())); #[cfg(not(unix))] args.push(OsString::from(String::from_utf8_lossy(bytes).into_owned())); } Ok(args) } /// C `main()` entry point retained by the small `programs/zstdcli.c` forwarder. #[no_mangle] pub unsafe extern "C" fn ZSTD_rust_cli_main(arg_count: c_int, argv: *const *const c_char) -> c_int { if let Err(error) = check_lib_version() { eprintln!("zstd: {error}"); return 1; } match unsafe { argv_to_os_strings(arg_count, argv) } { Ok(args) => run_from_args(args), Err(error) => { eprintln!("zstd: {error}"); 1 } } } #[cfg(test)] mod tests { use super::*; fn parse(values: &[&str]) -> Cli { let args = values.iter().map(OsString::from).collect(); match parse_args(args).expect("arguments should parse") { Action::Run(cli) => *cli, Action::Help { .. } | Action::Version { .. } => panic!("expected a run action"), } } #[test] fn defaults_preserve_the_c_fileio_contract() { let cli = parse(&["zstd", "input"]); assert_eq!(cli.content_size, 1); assert_eq!(cli.workers, None); assert_eq!(cli.operation, Operation::Compress); assert_eq!(cli.compression_format, CompressionFormat::Zstd); } #[test] fn normal_help_ceiling_excludes_ultra_levels() { assert_eq!(max_c_level_for_help(), 19); } #[test] fn format_option_selects_frame_type_and_suffix() { let cli = parse(&["zstd", "--format=gzip", "input"]); assert_eq!(cli.compression_format, CompressionFormat::Gzip); assert_eq!(cli.compression_format.fio_type(), FIO_GZIP_COMPRESSION); assert_eq!(cli.compression_format.suffix(), b".gz\0"); let cli = parse(&["zstd", "--format=gzip", "--format=zstd", "input"]); assert_eq!(cli.compression_format, CompressionFormat::Zstd); } #[test] fn format_option_rejects_unknown_formats() { let error = parse_args(vec![ OsString::from("zstd"), OsString::from("--format=zip"), OsString::from("input"), ]) .expect_err("unknown formats must be rejected"); assert!(error.contains("unknown compression format")); } #[test] fn trace_option_accepts_attached_and_separate_paths() { let cli = parse(&["zstd", "--trace=first.trace", "input"]); assert_eq!( cli.trace.as_deref().map(CStr::to_bytes), Some(&b"first.trace"[..]) ); let cli = parse(&["zstd", "--trace", "second.trace", "input"]); assert_eq!( cli.trace.as_deref().map(CStr::to_bytes), Some(&b"second.trace"[..]) ); } #[test] fn long_mode_uses_the_cli_default_window_and_enables_ultra() { let cli = parse(&["zstd", "--long", "input"]); assert!(cli.ldm); assert!(cli.ultra); assert_eq!(cli.compression_params.windowLog, DEFAULT_LONG_WINDOW_LOG); } #[test] fn long_mode_does_not_replace_an_explicit_window_log() { let cli = parse(&["zstd", "--zstd=wlog=25", "--long", "input"]); assert_eq!(cli.compression_params.windowLog, 25); } #[test] fn max_mode_sets_the_full_c_and_ldm_parameter_bundle() { let cli = parse(&["zstd", "--max", "input"]); assert!(cli.ultra); assert!(cli.ldm); assert_eq!( cli.compression_params, ZSTD_compressionParameters { windowLog: MAX_WINDOW_LOG, chainLog: MAX_CHAIN_LOG, hashLog: MAX_HASH_LOG, searchLog: MAX_SEARCH_LOG, minMatch: MAX_MIN_MATCH, targetLength: MAX_TARGET_LENGTH, strategy: MAX_STRATEGY, } ); assert_eq!(cli.overlap_log, Some(MAX_OVERLAP_LOG)); assert_eq!(cli.ldm_hash_log, Some(MAX_LDM_HASH_LOG)); assert_eq!(cli.ldm_hash_rate_log, Some(0)); assert_eq!(cli.ldm_min_match, Some(MAX_LDM_MIN_MATCH)); assert_eq!(cli.ldm_bucket_size_log, Some(MAX_LDM_BUCKET_SIZE_LOG)); } #[test] fn max_mode_is_applied_at_the_option_position() { let cli = parse(&["zstd", "--max", "--zstd=wlog=25", "input"]); assert_eq!(cli.compression_params.windowLog, 25); let cli = parse(&["zstd", "--zstd=wlog=25", "--max", "input"]); assert_eq!(cli.compression_params.windowLog, MAX_WINDOW_LOG); } #[test] fn console_and_file_stat_compatibility_switches_are_recorded() { let cli = parse(&[ "zstd", "--fake-stderr-is-console", "--trace-file-stat", "input", ]); assert!(cli.fake_stderr_is_console); assert!(cli.trace_file_stat); } #[test] fn gzip_best_is_an_alias_for_level_nine() { let cli = parse(&["gzip", "--best", "input"]); assert!(cli.gzip_compat); assert_eq!(cli.level, 9); let error = parse_args(vec![OsString::from("zstd"), OsString::from("--best")]) .expect_err("--best is a gzip compatibility option"); assert!(error.contains("Incorrect parameter: --best")); } #[test] fn stdio_and_dictionary_short_options_are_preserved() { let cli = parse(&["zstd", "-dc", "-D", "dict", "-"]); assert_eq!(cli.operation, Operation::Decompress); assert!(is_stdout(cli.output.as_ref())); assert_eq!( cli.dictionary.as_deref().map(CStr::to_bytes), Some(&b"dict"[..]) ); assert_eq!( cli.inputs .iter() .map(|input| input.as_bytes()) .collect::>(), vec![STDIN_MARK.as_bytes()] ); } #[test] fn patch_from_accepts_attached_and_separate_reference_paths() { let attached = parse(&["zstd", "--patch-from=reference", "input"]); assert_eq!( attached.patch_from.as_deref().map(CStr::to_bytes), Some(&b"reference"[..]) ); assert!(attached.ultra); let separate = parse(&["zstd", "--patch-from", "reference", "input"]); assert_eq!( separate.patch_from.as_deref().map(CStr::to_bytes), Some(&b"reference"[..]) ); assert!(separate.ultra); } #[test] fn patch_from_rejects_conflicts_with_dictionary_mode_in_both_orders() { for values in [ &["zstd", "-D", "dict", "--patch-from=reference", "input"][..], &["zstd", "--patch-from", "reference", "-D", "dict", "input"][..], ] { let args = values.iter().map(OsString::from).collect(); let error = parse_args(args).expect_err("-D and --patch-from must conflict"); assert!(error.contains("can't use -D and --patch-from=# at the same time")); } } #[test] fn stdout_selection_does_not_enable_force_or_pass_through() { let cli = parse(&["zstd", "-c", "input"]); assert!(cli.force_stdout); assert!(!cli.force); assert_eq!(cli.pass_through, None); } #[test] fn short_level_can_be_combined_with_other_flags() { let cli = parse(&["zstd", "-5q", "input"]); assert_eq!(cli.level, 5); assert_eq!(cli.display_level, 1); } #[test] fn fast_levels_preserve_the_unsigned_32_bit_conversion() { let cli = parse(&["zstd", "--fast", "-4294967295", "input"]); assert_eq!(cli.level, -1); let cli = parse(&["zstd", "--fast=4294967295", "input"]); assert_eq!(cli.level, -MAX_FAST_ACCELERATION); } #[test] fn short_option_equals_form_is_accepted() { let cli = parse(&["zstd", "-T=2", "-M=64M", "-B=1M", "input"]); assert_eq!(cli.workers, Some(2)); assert_eq!(cli.mem_limit, Some(64 << 20)); assert_eq!(cli.block_size, Some(1 << 20)); } #[test] fn valueless_long_flags_reject_attached_values() { let error = parse_args(vec![OsString::from("zstd"), OsString::from("--rm=0")]) .expect_err("an attached value must not activate --rm"); assert!(error.contains("does not take an argument")); } #[test] fn empty_attached_adapt_is_rejected_but_bare_adapt_is_enabled() { let bare = parse(&["zstd", "--adapt", "input"]); assert!(bare.adapt); let error = parse_args(vec![OsString::from("zstd"), OsString::from("--adapt=")]) .expect_err("an empty attached adapt value must be rejected"); assert!(error.contains("Incorrect parameter: --adapt=")); } #[test] fn adapt_bounds_and_rsyncable_single_thread_are_parsed_for_runtime_checks() { let error = parse_args(vec![ OsString::from("zstd"), OsString::from("--adapt=min=10,max=9"), ]) .expect_err("incoherent adaptation bounds must be rejected"); assert!(error.contains("Incorrect parameter: --adapt=min=10,max=9")); let cli = parse(&["zstd", "--rsyncable", "--single-thread", "input"]); assert!(cli.rsyncable); assert!(cli.single_thread); assert_eq!(cli.workers, Some(0)); } #[test] fn zstdmt_uses_auto_threads() { let cli = parse(&["zstdmt", "input"]); assert_eq!(cli.workers, Some(0)); assert!(!cli.single_thread); } #[test] fn auto_threads_selects_core_family() { let physical = parse(&["zstd", "-T0", "--auto-threads=physical", "input"]); assert_eq!(physical.workers, Some(0)); assert!(!physical.auto_threads_logical); let logical = parse(&["zstd", "-T0", "--auto-threads", "logical", "input"]); assert_eq!(logical.workers, Some(0)); assert!(logical.auto_threads_logical); } #[test] fn auto_threads_rejects_unknown_core_family() { let error = parse_args(vec![ OsString::from("zstd"), OsString::from("--auto-threads=bogus"), ]) .expect_err("an unknown core family must be rejected"); assert!(error.contains("unknown auto-thread mode")); } #[test] fn single_thread_pins_zero_workers() { let cli = parse(&["zstd", "--single-thread", "input"]); assert_eq!(cli.workers, Some(0)); assert!(cli.single_thread); } #[test] fn a_later_thread_count_overrides_single_thread_workers() { /* Mirrors the C CLI: -T after --single-thread wins the worker count, * while the single-thread latch stays set. */ let cli = parse(&["zstd", "--single-thread", "-T2", "input"]); assert_eq!(cli.workers, Some(2)); assert!(cli.single_thread); } #[test] fn single_thread_rejects_attached_values() { let error = parse_args(vec![ OsString::from("zstd"), OsString::from("--single-thread=1"), ]) .expect_err("an attached value must not activate --single-thread"); assert!(error.contains("does not take an argument")); } #[test] fn alternate_format_aliases_select_compatibility_modes() { let cli = Cli::new("gzip"); assert_eq!(cli.compression_format, CompressionFormat::Gzip); assert_eq!(cli.operation, Operation::Compress); assert!(cli.remove_source); let cli = Cli::new("unxz"); assert_eq!(cli.compression_format, CompressionFormat::Xz); assert_eq!(cli.operation, Operation::Decompress); assert!(cli.remove_source); let cli = Cli::new("lzma"); assert_eq!(cli.compression_format, CompressionFormat::Lzma); assert_eq!(cli.operation, Operation::Compress); } #[cfg(unix)] #[test] fn short_path_arguments_keep_non_utf8_bytes() { let output = OsString::from_vec(vec![b'-', b'o', 0xff, b'.', b'z', b's', b't']); let action = parse_args(vec![ OsString::from("zstd"), output, OsString::from("input"), ]) .expect("arguments should parse"); let Action::Run(cli) = action else { panic!("expected a run action"); }; assert_eq!( cli.output.as_deref().map(CStr::to_bytes), Some(&b"\xff.zst"[..]) ); } #[test] fn train_defaults_to_fastcover_and_dictionary_output() { let cli = parse(&["zstd", "--train", "sample"]); assert_eq!(cli.operation, Operation::Train); assert_eq!(cli.training_algorithm, TrainingAlgorithm::FastCover); assert_eq!( cli.output.as_deref().map(CStr::to_bytes), Some(&b"dictionary"[..]) ); assert_eq!(cli.max_dict_size, DEFAULT_MAX_DICT_SIZE); assert_eq!(cli.fast_cover_params.d, 8); assert_eq!(cli.fast_cover_params.f, 20); assert_eq!(cli.fast_cover_params.steps, 4); assert_eq!(cli.fast_cover_params.splitPoint, 0.75); assert_eq!(cli.fast_cover_params.accel, DEFAULT_FASTCOVER_ACCEL); } #[test] fn train_cover_parameters_select_algorithm_and_parse_optional_fields() { let cli = parse(&[ "zstd", "--train-cover=k=48,d=8,steps=32,split=80,shrink=3", "sample", ]); assert_eq!(cli.operation, Operation::Train); assert_eq!(cli.training_algorithm, TrainingAlgorithm::Cover); assert_eq!(cli.cover_params.k, 48); assert_eq!(cli.cover_params.d, 8); assert_eq!(cli.cover_params.steps, 32); assert_eq!(cli.cover_params.splitPoint, 0.8); assert_eq!(cli.cover_params.shrinkDict, 1); assert_eq!(cli.cover_params.shrinkDictMaxRegression, 3); } #[test] fn train_fastcover_and_legacy_parameter_forms_share_training_options() { let fast = parse(&[ "zstd", "--train-fastcover=k=64,d=8,f=21,steps=12,split=75,accel=2,shrink", "--maxdict", "128K", "--dictID=42", "-B4K", "-T2", "-o", "custom.dict", "sample", ]); assert_eq!(fast.training_algorithm, TrainingAlgorithm::FastCover); assert_eq!(fast.fast_cover_params.k, 64); assert_eq!(fast.fast_cover_params.d, 8); assert_eq!(fast.fast_cover_params.f, 21); assert_eq!(fast.fast_cover_params.steps, 12); assert_eq!(fast.fast_cover_params.splitPoint, 0.75); assert_eq!(fast.fast_cover_params.accel, 2); assert_eq!(fast.fast_cover_params.shrinkDict, 1); assert_eq!(fast.fast_cover_params.shrinkDictMaxRegression, 1); assert_eq!(fast.max_dict_size, 128 << 10); assert_eq!(fast.dictionary_id, 42); assert_eq!(fast.block_size, Some(4 << 10)); assert_eq!(fast.workers, Some(2)); assert_eq!( fast.output.as_deref().map(CStr::to_bytes), Some(&b"custom.dict"[..]) ); let legacy = parse(&["zstd", "--train-legacy=selectivity=12", "sample"]); assert_eq!(legacy.operation, Operation::Train); assert_eq!(legacy.training_algorithm, TrainingAlgorithm::Legacy); assert_eq!(legacy.dictionary_selectivity, 12); let legacy_short = parse(&["zstd", "--train-legacy", "-s5", "sample"]); assert_eq!(legacy_short.training_algorithm, TrainingAlgorithm::Legacy); assert_eq!(legacy_short.dictionary_selectivity, 5); } #[test] fn training_options_preserve_upstream_validation_boundaries() { let attached = parse_args(vec![ OsString::from("zstd"), OsString::from("--train=value"), ]) .expect_err("--train does not accept an attached value"); assert!(attached.contains("does not take an argument")); let unknown = parse_args(vec![ OsString::from("zstd"), OsString::from("--train-cover=unknown=1"), ]) .expect_err("unknown cover parameters must be rejected"); assert!(unknown.contains("unknown --train-cover parameter")); let missing = parse_args(vec![OsString::from("zstd"), OsString::from("--dictID")]) .expect_err("--dictID requires a value"); assert!(missing.contains("missing argument for --dictID")); } #[test] fn recursive_flag_can_be_aggregated_with_other_shorts() { let cli = parse(&["zstd", "-rq", "dir"]); assert!(cli.recursive); assert_eq!(cli.display_level, 1); } #[test] fn list_mode_is_selected_by_short_and_long_flags() { let short = parse(&["zstd", "-l", "file.zst"]); let long = parse(&["zstd", "--list", "file.zst"]); assert_eq!(short.operation, Operation::List); assert_eq!(long.operation, Operation::List); } #[test] fn list_rejects_attached_values() { let error = parse_args(vec![OsString::from("zstd"), OsString::from("--list=x")]) .expect_err("an attached value must not activate --list"); assert!(error.contains("does not take an argument")); } #[test] fn show_default_cparams_is_a_compression_information_flag() { let cli = parse(&["zstd", "--show-default-cparams", "input"]); assert!(cli.show_default_cparams); assert_eq!(cli.operation, Operation::Compress); } #[test] fn show_default_cparams_rejects_attached_values() { let error = parse_args(vec![ OsString::from("zstd"), OsString::from("--show-default-cparams=x"), ]) .expect_err("show-default-cparams must not consume an attached value"); assert!(error.contains("does not take an argument")); } #[test] fn filelist_accumulates_both_syntaxes() { let cli = parse(&["zstd", "--filelist=one.txt", "--filelist", "two.txt", "in"]); assert_eq!( cli.file_lists .iter() .map(|list| list.as_bytes()) .collect::>(), vec![&b"one.txt"[..], &b"two.txt"[..]] ); } #[test] fn filelist_argument_must_not_be_another_option() { let error = parse_args(vec![ OsString::from("zstd"), OsString::from("--filelist"), OsString::from("-q"), ]) .expect_err("an option must not be consumed as the list name"); assert!(error.contains("cannot be separated from its argument")); } #[test] fn output_dir_flat_accepts_attached_and_separate_values() { let attached = parse(&["zstd", "--output-dir-flat=out", "in"]); let separate = parse(&["zstd", "--output-dir-flat", "out", "in"]); assert_eq!( attached.output_dir_flat.as_deref().map(CStr::to_bytes), Some(&b"out"[..]) ); assert_eq!( separate.output_dir_flat.as_deref().map(CStr::to_bytes), Some(&b"out"[..]) ); } #[test] fn output_dir_flat_rejects_an_empty_name() { /* Mirrors the C NEXT_FIELD semantics: `--output-dir-flat=` keeps the * empty attached value instead of consuming the next argument. */ let error = parse_args(vec![ OsString::from("zstd"), OsString::from("--output-dir-flat="), OsString::from("out"), ]) .expect_err("an empty output dir must be rejected"); assert!(error.contains("output dir cannot be empty string")); } #[cfg(unix)] #[test] fn output_dir_mirror_is_parsed_on_posix_targets() { let cli = parse(&["zstd", "--output-dir-mirror", "out", "in"]); assert_eq!( cli.output_dir_mirror.as_deref().map(CStr::to_bytes), Some(&b"out"[..]) ); } #[test] fn bench_mode_parses_level_duration_and_defaults() { let cli = parse(&["zstd", "-b1", "-i0", "input"]); assert_eq!(cli.operation, Operation::Bench); assert_eq!(cli.level, 1); assert_eq!(cli.bench_nb_seconds, Some(0)); assert_eq!(cli.bench_end_level, None); assert_eq!( cli.inputs .iter() .map(|input| input.as_bytes()) .collect::>(), vec![&b"input"[..]] ); } #[test] fn bench_range_aggregates_within_a_single_argument() { let cli = parse(&["zstd", "-b5e6i2", "input"]); assert_eq!(cli.operation, Operation::Bench); assert_eq!(cli.level, 5); assert_eq!(cli.bench_end_level, Some(6)); assert_eq!(cli.bench_nb_seconds, Some(2)); } #[test] fn bench_duration_without_digits_defaults_to_zero() { let cli = parse(&["zstd", "-b", "-e", "-i"]); assert_eq!(cli.operation, Operation::Bench); assert_eq!(cli.level, DEFAULT_CLEVEL); assert_eq!(cli.bench_end_level, Some(0)); assert_eq!(cli.bench_nb_seconds, Some(0)); } #[test] fn bench_decompression_keeps_benchmark_operation() { let cli = parse(&["zstd", "-b", "-d", "-i0", "input"]); assert_eq!(cli.operation, Operation::Bench); assert!(cli.bench_decode); } #[cfg(feature = "benchmark")] #[test] fn benchmark_level_normalization_matches_c_bridge() { assert_eq!( normalize_benchmark_levels(100, 4, crate::benchzstd::BMK_both, 22), (22, 22) ); assert_eq!( normalize_benchmark_levels(7, 3, crate::benchzstd::BMK_both, 22), (7, 7) ); assert_eq!( normalize_benchmark_levels(7, 3, crate::benchzstd::BMK_decodeOnly, 22), (0, 0) ); } #[cfg(not(feature = "benchmark"))] #[test] fn benchmark_bridge_returns_unavailable_without_feature() { assert_eq!( unsafe { ZSTD_rust_cli_bench( ptr::null(), 0, ptr::null(), 3, 3, ptr::null(), 0, 3, 0, 1, 0, ) }, -1 ); } }