diff --git a/rust/src/zstd_cli.rs b/rust/src/zstd_cli.rs index 8fa1fc8cc..c9fd09b86 100644 --- a/rust/src/zstd_cli.rs +++ b/rust/src/zstd_cli.rs @@ -406,6 +406,7 @@ struct Cli { inputs: Vec, output: Option, dictionary: Option, + patch_from: Option, level: i32, ultra: bool, display_level: i32, @@ -470,6 +471,7 @@ impl Cli { inputs: Vec::new(), output: None, dictionary: None, + patch_from: None, level: default_level(), ultra: false, display_level: 2, @@ -1713,7 +1715,16 @@ fn parse_long_option( cli.trace_file_stat = true; Ok(None) } - "--patch-from" | "--priority" | "--fake-stdin-is-console" | "--fake-stdout-is-console" => { + "--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) } @@ -1804,7 +1815,14 @@ fn parse_short_options( .unwrap_or_else(|| next_os_value(args, index, &format!("-{option}")))?; match option { 'o' => cli.output = Some(os_cstring(&argument)?), - 'D' => cli.dictionary = 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 = @@ -2004,10 +2022,7 @@ unsafe fn apply_preferences(cli: &Cli, prefs: *mut FIO_prefs_t, ctx: *mut FIO_ct if let Some(value) = cli.pass_through { FIO_setPassThroughFlag(prefs, value); } - /* `FIO_createPreferences()` leaves this field unspecified. The Rust - * frontend does not yet parse --patch-from, so explicitly keep the - * C file-I/O backend in ordinary dictionary mode. */ - FIO_setPatchFromMode(prefs, 0); + 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); @@ -2065,6 +2080,10 @@ 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 { @@ -2138,7 +2157,7 @@ fn check_terminal_safety(cli: &Cli) -> Result<(), String> { #[cfg(feature = "compression")] unsafe fn print_default_cparams(cli: &Cli) { - let dict_size = cli.dictionary.as_ref().map_or(0, |dictionary| unsafe { + let dict_size = fileio_dictionary(cli).map_or(0, |dictionary| unsafe { UTIL_getFileSize(dictionary.as_ptr()) as usize }); for input in &cli.inputs { @@ -2165,7 +2184,7 @@ unsafe fn print_default_cparams(cli: &Cli) { #[cfg(feature = "compression")] unsafe fn print_actual_cparams(cli: &Cli) { - let dict_size = cli.dictionary.as_ref().map_or(0, |dictionary| unsafe { + let dict_size = fileio_dictionary(cli).map_or(0, |dictionary| unsafe { UTIL_getFileSize(dictionary.as_ptr()) as usize }); for input in &cli.inputs { @@ -2746,6 +2765,10 @@ fn run_cli(mut cli: Cli) -> Result { 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() { @@ -2778,10 +2801,7 @@ fn run_cli(mut cli: Cli) -> Result { .output .as_ref() .map_or(ptr::null(), |value| value.as_ptr()); - let dictionary = cli - .dictionary - .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 => { @@ -3037,6 +3057,35 @@ mod tests { ); } + #[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"]);