dd: use seek for stdin skip when possible (#9821)

* dd: use seek for stdin skip when possible

* Add Rust tests for skip with seekable stdin

* Address review comments: fix return value, add comment, refactor tests

* dd: use ibs-sized buffer for ESPIPE fallback in skip
This commit is contained in:
Chris Dryden
2026-01-13 18:59:21 +01:00
committed by GitHub
parent 603d37480a
commit 5fc9f8e7e0
2 changed files with 66 additions and 7 deletions
+33 -7
View File
@@ -272,14 +272,40 @@ impl Source {
return Ok(len);
}
}
let m = read_and_discard(f, n, ibs)?;
if m < n {
show_error!(
"{}",
translate!("dd-error-cannot-skip-offset", "file" => "standard input")
);
// 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 using ibs-sized buffer
Some(Err(e)) if e.raw_os_error() == Some(libc::ESPIPE) => {
let m = read_and_discard(f, n, ibs)?;
if m < n {
show_error!(
"{}",
translate!("dd-error-cannot-skip-offset", "file" => "standard input")
);
}
Ok(m)
}
_ => {
show_error!(
"{}",
translate!("dd-error-cannot-skip-invalid", "file" => "standard input")
);
set_exit_code(1);
Ok(0)
}
}
Ok(m)
}
Self::File(f) => f.seek(SeekFrom::Current(n.try_into().unwrap())),
#[cfg(unix)]
+33
View File
@@ -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!();