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
2 changes: 2 additions & 0 deletions src/uu/dd/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ dd-error-failed-to-open = failed to open { $path }
dd-error-write-error = write error
dd-error-failed-to-seek = failed to seek in output file
dd-error-io-error = IO error
dd-error-reading = error reading { $file }
dd-error-writing = error writing { $file }
dd-error-cannot-skip-offset = '{ $file }': cannot skip to specified offset
dd-error-cannot-skip-invalid = '{ $file }': cannot skip: Invalid argument
dd-error-cannot-seek-invalid = '{ $output }': cannot seek: Invalid argument
Expand Down
45 changes: 36 additions & 9 deletions src/uu/dd/src/dd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use std::time::{Duration, Instant};
use clap::{Arg, Command};
use gcd::Gcd;
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult};
use uucore::error::{FromIo, UError, UResult};
#[cfg(unix)]
use uucore::error::{USimpleError, set_exit_code};
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
Expand Down Expand Up @@ -1133,7 +1133,7 @@ fn flush_caches_full_length(i: &Input, o: &Output) {
///
/// If there is a problem reading from the input or writing to
/// this output.
fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
fn dd_copy(mut i: Input, o: Output) -> UResult<()> {
// The read and write statistics.
//
// These objects are counters, initialized to zero. After each
Expand Down Expand Up @@ -1190,7 +1190,8 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
&prog_tx,
output_thread,
truncate,
);
)
.map_err_context(|| translate!("dd-error-io-error"));
}

// Spawn a timer thread to provide a scheduled signal indicating when we
Expand Down Expand Up @@ -1221,17 +1222,31 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
let output_nocache = o.settings.oflags.nocache;
let output_direct = o.settings.oflags.direct;

// How the input and the output are named in error messages, as in GNU
// `dd`: the operand itself when given, `'standard input'` or
// `'standard output'` otherwise.
let input_name = i.settings.infile.as_deref().map_or_else(
|| translate!("dd-standard-input"),
|f| f.quote().to_string(),
);
let output_name = i.settings.outfile.as_deref().map_or_else(
|| translate!("dd-standard-output"),
|f| f.quote().to_string(),
);

// Add partial block buffering, if needed.
let mut o = if o.settings.buffered {
BlockWriter::Buffered(BufferedOutput::new(o)?)
BlockWriter::Buffered(
BufferedOutput::new(o).map_err_context(|| translate!("dd-error-io-error"))?,
)
} else {
BlockWriter::Unbuffered(o)
};

// Aligned read scratch sized to the block size (the max size needed).
// 4 KiB alignment satisfies block devices that enforce a strict
// `dma_alignment` for `iflag=direct` reads — see `AlignedBuf`.
let mut buf = AlignedBuf::new(bsize)?;
let mut buf = AlignedBuf::new(bsize).map_err_context(|| translate!("dd-error-io-error"))?;
// Separate scratch for `conv=block` / `conv=unblock`, which can change
// the byte count and so cannot be done in-place in `buf`.
let mut conv_buf: Vec<u8> = Vec::new();
Expand All @@ -1243,7 +1258,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
// each iteration and cumulative statistics are reported to
// the progress reporting thread.
// A failure ends the loop, so the statistics gathered so far still get reported.
let mut copy_error = None;
let mut copy_error: Option<Box<dyn UError>> = None;
while below_count_limit(i.settings.count, &rstat) {
// Read a block from the input then write the block to the output.
//
Expand All @@ -1252,7 +1267,11 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
// blocks already read and the number of blocks remaining.
let loop_bsize = calc_loop_bsize(i.settings.count, &rstat, i.settings.ibs, bsize);
let Ok((rstat_update, data)) = read_helper(&mut i, &mut buf, &mut conv_buf, loop_bsize)
.map_err(|e| copy_error = Some(e))
.map_err(|e| {
copy_error = Some(e.map_err_context(
|| translate!("dd-error-reading", "file" => input_name.clone()),
));
})
else {
break;
};
Expand All @@ -1265,7 +1284,14 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
}
break;
}
let Ok(wstat_update) = o.write_blocks(data).map_err(|e| copy_error = Some(e)) else {
let Ok(wstat_update) = o.write_blocks(data).map_err(|e| {
copy_error = Some(
e.map_err_context(|| translate!("dd-error-writing", "file" => output_name.clone())),
);
}) else {
// The block was read before the write failed, so it still counts as
// a record in, as GNU `dd` reports it.
rstat += rstat_update;
break;
};

Expand Down Expand Up @@ -1325,6 +1351,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
}

finalize(o, rstat, wstat, start, &prog_tx, output_thread, truncate)
.map_err_context(|| translate!("dd-error-io-error"))
}

/// Flush output, print final stats, and join with the progress thread.
Expand Down Expand Up @@ -1600,7 +1627,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
None if is_stdout_redirected_to_seekable_file() => Output::new_file_from_stdout(&settings)?,
None => Output::new_stdout(&settings)?,
};
dd_copy(i, o).map_err_context(|| translate!("dd-error-io-error"))
dd_copy(i, o)
}

pub fn uu_app() -> Command {
Expand Down
17 changes: 17 additions & 0 deletions tests/by-util/test_dd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2300,9 +2300,26 @@ fn test_stats_are_reported_when_a_write_fails() {
// second one is cut short at 256 KiB, and the third write fails.
result.stderr_contains("1+1 records out");
result.stderr_contains("786432 bytes");
// The third block was read before its write failed, so it counts as a
// record in.
result.stderr_contains("3+0 records in");
result.stderr_contains("error writing 'capped.bin'");
assert_eq!(at.metadata("capped.bin").len(), CAP);
}

// A failing read names the operation and the input, as GNU `dd` does, instead
// of reporting a bare "IO error".
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn test_read_error_names_the_input() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("subdir");
ucmd.args(&["if=subdir", "of=/dev/null"])
.fails()
.stderr_contains("error reading 'subdir'")
.stderr_contains("0+0 records in");
}

#[cfg(all(feature = "feat_diagnostics", not(wasi_runner)))]
mod diagnostics {
use super::*;
Expand Down
Loading