refactor: name the output/input paths passed to OutSink

OutSink::open_with_options took two adjacent positional Option<&Path>
parameters (output_file, input_file). The type system cannot tell them
apart, so a future call site that swaps them still compiles - and the
same-file-aliasing guard would then check the wrong path, silently
re-enabling the accidental-overwrite protection bypass it exists to
prevent. All three current call sites were correct; this hardens the
signature before a wrong one appears.

Introduce OutSinkPaths, a struct with named output_file/input_file
fields, so every call site must label which path plays which role.

No behavior change.

Test plan: cargo clippy (default/--tests) clean; cargo test passes
all 56 tests.
This commit is contained in:
2026-06-12 22:53:29 +02:00
parent 2f16e735c3
commit 655013f86e
2 changed files with 25 additions and 10 deletions
+12 -6
View File
@@ -179,8 +179,10 @@ pub fn encrypt(
let plaintext_length = input.length;
let mut f_plain = AheadReader::from(input.reader, chunk_sz);
let mut f_encrypted = OutSink::open_with_options(
options.output_file.as_deref(),
options.input_file.as_deref(),
OutSinkPaths {
output_file: options.output_file.as_deref(),
input_file: options.input_file.as_deref(),
},
&options.output,
)?;
@@ -292,8 +294,10 @@ pub fn decrypt(
let mut f_encrypted = AheadReader::from(reader, cipher_chunk);
let mut f_plain = OutSink::open_with_options(
options.output_file.as_deref(),
options.input_file.as_deref(),
OutSinkPaths {
output_file: options.output_file.as_deref(),
input_file: options.input_file.as_deref(),
},
&options.output,
)?;
@@ -423,8 +427,10 @@ pub fn decrypt_range(
let last_idx = n_chunks - 1;
let mut out = OutSink::open_with_options(
options.output_file.as_deref(),
Some(&options.input_file),
OutSinkPaths {
output_file: options.output_file.as_deref(),
input_file: Some(&options.input_file),
},
&options.output,
)?;
+13 -4
View File
@@ -198,6 +198,16 @@ fn output_aliases_input(output: &Path, input: Option<&Path>) -> io::Result<bool>
}
}
/// Paths for [`OutSink::open_with_options`], named so the output and input
/// roles cannot be swapped silently at a call site (both are `Option<&Path>`).
pub(crate) struct OutSinkPaths<'a> {
/// Where the finished output is renamed to; `None` means stdout.
pub output_file: Option<&'a Path>,
/// The input being read; only consulted by the aliasing guard that
/// permits in-place overwrite of the input without `--force`.
pub input_file: Option<&'a Path>,
}
/// Output sink that supports atomic file replacement.
///
/// For file outputs: bytes are written to a private, randomly named temp file.
@@ -219,11 +229,10 @@ pub(crate) enum OutSink {
impl OutSink {
pub(crate) fn open_with_options(
output_file: Option<&Path>,
input_file: Option<&Path>,
paths: OutSinkPaths<'_>,
options: &OutputOptions,
) -> io::Result<Self> {
match output_file {
match paths.output_file {
None if options.buffer_verify_stdout => {
let dir = temp_dir_for_stdout(options.temp_dir.as_deref());
Ok(Self::BufferVerify {
@@ -235,7 +244,7 @@ impl OutSink {
let final_path = f.to_path_buf();
if final_path.exists()
&& !options.force
&& !output_aliases_input(&final_path, input_file)?
&& !output_aliases_input(&final_path, paths.input_file)?
{
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,