Skip to content

Commit 5fc9f8e

Browse files
authored
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
1 parent 603d374 commit 5fc9f8e

2 files changed

Lines changed: 66 additions & 7 deletions

File tree

src/uu/dd/src/dd.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -272,14 +272,40 @@ impl Source {
272272
return Ok(len);
273273
}
274274
}
275-
let m = read_and_discard(f, n, ibs)?;
276-
if m < n {
277-
show_error!(
278-
"{}",
279-
translate!("dd-error-cannot-skip-offset", "file" => "standard input")
280-
);
275+
// Get file length before seeking to avoid race condition
276+
let file_len = f.metadata().map(|m| m.len()).unwrap_or(u64::MAX);
277+
// Try seek first; fall back to read if not seekable
278+
match n.try_into().ok().map(|n| f.seek(SeekFrom::Current(n))) {
279+
Some(Ok(pos)) => {
280+
if pos > file_len {
281+
show_error!(
282+
"{}",
283+
translate!("dd-error-cannot-skip-offset", "file" => "standard input")
284+
);
285+
}
286+
Ok(n)
287+
}
288+
// ESPIPE means the file descriptor is not seekable (e.g., a pipe),
289+
// so fall back to reading and discarding bytes using ibs-sized buffer
290+
Some(Err(e)) if e.raw_os_error() == Some(libc::ESPIPE) => {
291+
let m = read_and_discard(f, n, ibs)?;
292+
if m < n {
293+
show_error!(
294+
"{}",
295+
translate!("dd-error-cannot-skip-offset", "file" => "standard input")
296+
);
297+
}
298+
Ok(m)
299+
}
300+
_ => {
301+
show_error!(
302+
"{}",
303+
translate!("dd-error-cannot-skip-invalid", "file" => "standard input")
304+
);
305+
set_exit_code(1);
306+
Ok(0)
307+
}
281308
}
282-
Ok(m)
283309
}
284310
Self::File(f) => f.seek(SeekFrom::Current(n.try_into().unwrap())),
285311
#[cfg(unix)]

tests/by-util/test_dd.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,39 @@ fn test_skip_beyond_file() {
669669
);
670670
}
671671

672+
#[test]
673+
#[cfg(unix)]
674+
fn test_skip_beyond_file_seekable_stdin() {
675+
// When stdin is a seekable file, dd should use seek to skip bytes.
676+
// This tests that skipping beyond the file size issues a warning.
677+
use std::process::Stdio;
678+
679+
// Test cases: (bs, skip) pairs that skip beyond a 4-byte file
680+
let test_cases = [
681+
("bs=1", "skip=5"), // skip 5 bytes
682+
("bs=3", "skip=2"), // skip 6 bytes
683+
];
684+
685+
for (bs, skip) in test_cases {
686+
let (at, mut ucmd) = at_and_ucmd!();
687+
at.write("in", "abcd");
688+
689+
let stdin = OwnedFileDescriptorOrHandle::open_file(
690+
OpenOptions::new().read(true),
691+
at.plus("in").as_path(),
692+
)
693+
.unwrap();
694+
695+
ucmd.args(&[bs, skip, "count=0", "status=noxfer"])
696+
.set_stdin(Stdio::from(stdin))
697+
.succeeds()
698+
.no_stdout()
699+
.stderr_contains(
700+
"'standard input': cannot skip to specified offset\n0+0 records in\n0+0 records out\n",
701+
);
702+
}
703+
}
704+
672705
#[test]
673706
fn test_seek_do_not_overwrite() {
674707
let (at, mut ucmd) = at_and_ucmd!();

0 commit comments

Comments
 (0)