Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,21 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
let file_name = path.file_name().ok_or_else(|| Error::Custom {
reason: FinalError::with_title(format!("{} does not have a file name", PathFmt(path))),
})?;
files_output_paths.push(file_name.into());
let output_file_name = match <[u8] as ByteSlice>::from_os_str(file_name) {
Some(bytes) if !is_path_stdin(path) => {
let stripped = extension::strip_known_extensions_from_name(bytes, format.len());
if stripped == bytes {
utils::append_ascii_suffix_to_os_str(file_name, "-output")
} else {
stripped
.to_os_str()
.expect("stripped bytes came from an OsStr")
.to_owned()
}
}
_ => file_name.to_owned(),
};
files_output_paths.push(output_file_name.into());
files_extensions.push(format.clone());
}
} else {
Expand Down
14 changes: 14 additions & 0 deletions src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ fn split_extension_at_end(name: &[u8]) -> Option<(&[u8], Extension)> {
Some((new_name, ext))
}

/// Remove up to `max_count` trailing known extensions from a file name.
///
/// Used by `--format`, where the extensions in the path are not parsed, so the output name
/// still has to be derived from the input name to avoid overwriting the input file.
pub fn strip_known_extensions_from_name(mut name: &[u8], max_count: usize) -> &[u8] {
for _ in 0..max_count {
let Some((new_name, _)) = split_extension_at_end(name) else {
break;
};
name = new_name;
}
name
}

pub fn parse_format_flag(text: &str) -> Result<Vec<Extension>> {
let extensions: Vec<Extension> = text
.split('.')
Expand Down
41 changes: 41 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2089,3 +2089,44 @@ fn merging_a_rar_asks_before_replacing_each_file() {
.success();
assert_eq!("Testing 123\n", fs::read_to_string(out.join("testfile.txt")).unwrap());
}

/// Decompressing with `--format` must not write the output over the input file (issue #442).
#[test]
fn decompress_with_format_flag_does_not_overwrite_input() {
let (_tempdir, dir) = testdir().unwrap();

let source = dir.join("file");
fs::write(&source, "hello").unwrap();
let input = dir.join("file.zst.zst.zst");

crate::utils::cargo_bin()
.current_dir(dir)
.args(["compress", "--yes"])
.arg(&source)
.arg(&input)
.assert()
.success();

let archive_before = fs::read(&input).unwrap();

// Only the outermost `.zst` is undone, so the output name must be `file.zst.zst`.
// The exit status is not asserted here: when the output path collides with the input,
// the run truncates the input and then fails, and the assertions below must be the
// ones that report it.
let _ = crate::utils::cargo_bin_command()
.current_dir(dir)
.args(["decompress", "--yes", "--here", "--format", "zst"])
.arg(&input)
.status()
.unwrap();

assert_eq!(
fs::read(&input).unwrap(),
archive_before,
"the input archive must not be overwritten by its own decompressed output"
);
assert!(
dir.join("file.zst.zst").exists(),
"output should drop one known extension from the input name"
);
}