feat(cli): restore patch-from argument routing

Parse attached and separated --patch-from references, preserve the upstream ultra ceiling and conflict checks, and pass the reference path through the existing file-I/O dictionary ABI with patch mode enabled.

Test Plan:

- cargo test --manifest-path rust/cli/Cargo.toml

- cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets

- cargo +nightly fmt --manifest-path rust/Cargo.toml

- make -C tests -j2 check

- small attached/separate patch-from round-trip and rejection smoke
This commit is contained in:
2026-07-18 08:11:55 +02:00
parent b81109095f
commit b44b480f17
+61 -12
View File
@@ -406,6 +406,7 @@ struct Cli {
inputs: Vec<CString>,
output: Option<CString>,
dictionary: Option<CString>,
patch_from: Option<CString>,
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<i32, String> {
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<i32, String> {
.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"]);