diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 412b6668fe9..2f7183a3eb2 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -258,16 +258,41 @@ impl Source { return Ok(len); } } - match io::copy(&mut f.take(n), &mut io::sink()) { - Ok(m) if m < n => { + // Get file length before seeking to avoid race condition + let file_len = f.metadata().map(|m| m.len()).unwrap_or(u64::MAX); + // Try seek first; fall back to read if not seekable + match n.try_into().ok().map(|n| f.seek(SeekFrom::Current(n))) { + Some(Ok(pos)) => { + if pos > file_len { + show_error!( + "{}", + translate!("dd-error-cannot-skip-offset", "file" => "standard input") + ); + } + Ok(n) + } + // ESPIPE means the file descriptor is not seekable (e.g., a pipe), + // so fall back to reading and discarding bytes + Some(Err(e)) if e.raw_os_error() == Some(libc::ESPIPE) => { + match io::copy(&mut f.take(n), &mut io::sink()) { + Ok(m) if m < n => { + show_error!( + "{}", + translate!("dd-error-cannot-skip-offset", "file" => "standard input") + ); + Ok(m) + } + other => other, + } + } + _ => { show_error!( "{}", - translate!("dd-error-cannot-skip-offset", "file" => "standard input") + translate!("dd-error-cannot-skip-invalid", "file" => "standard input") ); - Ok(m) + set_exit_code(1); + Ok(0) } - Ok(m) => Ok(m), - Err(e) => Err(e), } } Self::File(f) => f.seek(SeekFrom::Current(n.try_into().unwrap())), diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index a6a52e66fb5..bda10d615cf 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -669,6 +669,39 @@ fn test_skip_beyond_file() { ); } +#[test] +#[cfg(unix)] +fn test_skip_beyond_file_seekable_stdin() { + // When stdin is a seekable file, dd should use seek to skip bytes. + // This tests that skipping beyond the file size issues a warning. + use std::process::Stdio; + + // Test cases: (bs, skip) pairs that skip beyond a 4-byte file + let test_cases = [ + ("bs=1", "skip=5"), // skip 5 bytes + ("bs=3", "skip=2"), // skip 6 bytes + ]; + + for (bs, skip) in test_cases { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("in", "abcd"); + + let stdin = OwnedFileDescriptorOrHandle::open_file( + OpenOptions::new().read(true), + at.plus("in").as_path(), + ) + .unwrap(); + + ucmd.args(&[bs, skip, "count=0", "status=noxfer"]) + .set_stdin(Stdio::from(stdin)) + .succeeds() + .no_stdout() + .stderr_contains( + "'standard input': cannot skip to specified offset\n0+0 records in\n0+0 records out\n", + ); + } +} + #[test] fn test_seek_do_not_overwrite() { let (at, mut ucmd) = at_and_ucmd!();