Skip to content
Merged
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
57 changes: 48 additions & 9 deletions src/uu/dd/src/dd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized fadvise Fadvise FADV DONTNEED ESPIPE
// spell-checker:ignore fname, ftype, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized fadvise Fadvise FADV DONTNEED ESPIPE

mod datastructures;
use datastructures::*;
Expand Down Expand Up @@ -49,6 +49,8 @@ use nix::{
fcntl::{posix_fadvise, PosixFadviseAdvice},
};
use uucore::display::Quotable;
#[cfg(unix)]
use uucore::error::set_exit_code;
use uucore::error::{FromIo, UResult};
use uucore::{format_usage, help_about, help_section, help_usage, show_error};
#[cfg(target_os = "linux")]
Expand Down Expand Up @@ -200,14 +202,25 @@ impl Source {
Err(e) => Err(e),
},
#[cfg(unix)]
Self::StdinFile(f) => match io::copy(&mut f.take(n), &mut io::sink()) {
Ok(m) if m < n => {
show_error!("'standard input': cannot skip to specified offset");
Ok(m)
Self::StdinFile(f) => {
if let Ok(Some(len)) = try_get_len_of_block_device(f) {
if len < n {
// GNU compatibility:
// this case prints the stats but sets the exit code to 1
show_error!("'standard input': cannot skip: Invalid argument");
set_exit_code(1);
return Ok(len);
}
}
Ok(m) => Ok(m),
Err(e) => Err(e),
},
match io::copy(&mut f.take(n), &mut io::sink()) {
Ok(m) if m < n => {
show_error!("'standard input': cannot skip to specified offset");
Ok(m)
}
Ok(m) => Ok(m),
Err(e) => Err(e),
}
}
Self::File(f) => f.seek(io::SeekFrom::Start(n)),
#[cfg(unix)]
Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()),
Expand Down Expand Up @@ -527,7 +540,19 @@ impl Dest {
fn seek(&mut self, n: u64) -> io::Result<u64> {
match self {
Self::Stdout(stdout) => io::copy(&mut io::repeat(0).take(n), stdout),
Self::File(f, _) => f.seek(io::SeekFrom::Start(n)),
Self::File(f, _) => {
#[cfg(unix)]
if let Ok(Some(len)) = try_get_len_of_block_device(f) {
if len < n {
// GNU compatibility:
// this case prints the stats but sets the exit code to 1
show_error!("'standard output': cannot seek: Invalid argument");
set_exit_code(1);
return Ok(len);
}
}
f.seek(io::SeekFrom::Start(n))
}
#[cfg(unix)]
Self::Fifo(f) => {
// Seeking in a named pipe means *reading* from the pipe.
Expand Down Expand Up @@ -1133,6 +1158,20 @@ fn is_stdout_redirected_to_seekable_file() -> bool {
}
}

/// Try to get the len if it is a block device
#[cfg(unix)]
fn try_get_len_of_block_device(file: &mut File) -> io::Result<Option<u64>> {
let ftype = file.metadata()?.file_type();
if !ftype.is_block_device() {
return Ok(None);
}

// FIXME: this can be replaced by file.stream_len() when stable.
let len = file.seek(SeekFrom::End(0))?;
file.rewind()?;
Ok(Some(len))
}

/// Decide whether the named file is a named pipe, also known as a FIFO.
#[cfg(unix)]
fn is_fifo(filename: &str) -> bool {
Expand Down
44 changes: 44 additions & 0 deletions tests/by-util/test_dd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// file that was distributed with this source code.
// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg

#[cfg(unix)]
use crate::common::util::run_ucmd_as_root_with_stdin_stdout;
use crate::common::util::TestScenario;
#[cfg(all(not(windows), feature = "printf"))]
use crate::common::util::{UCommand, TESTS_BINARY};
Expand Down Expand Up @@ -1566,3 +1568,45 @@ fn test_nocache_file() {
.succeeds()
.stderr_only("2048+0 records in\n2048+0 records out\n");
}

#[test]
#[cfg(unix)]
fn test_skip_past_dev() {
// NOTE: This test intends to trigger code which can only be reached with root permissions.
let ts = TestScenario::new(util_name!());

if let Ok(result) = run_ucmd_as_root_with_stdin_stdout(
&ts,
&["bs=1", "skip=10000000000000000", "count=0", "status=noxfer"],
Some("/dev/sda1"),
None,
) {
result.stderr_contains("dd: 'standard input': cannot skip: Invalid argument");
result.stderr_contains("0+0 records in");
result.stderr_contains("0+0 records out");
result.code_is(1);
} else {
print!("TEST SKIPPED");
}
}

#[test]
#[cfg(unix)]
fn test_seek_past_dev() {
// NOTE: This test intends to trigger code which can only be reached with root permissions.
let ts = TestScenario::new(util_name!());

if let Ok(result) = run_ucmd_as_root_with_stdin_stdout(
&ts,
&["bs=1", "seek=10000000000000000", "count=0", "status=noxfer"],
None,
Some("/dev/sda1"),
) {
result.stderr_contains("dd: 'standard output': cannot seek: Invalid argument");
result.stderr_contains("0+0 records in");
result.stderr_contains("0+0 records out");
result.code_is(1);
} else {
print!("TEST SKIPPED");
}
}
25 changes: 20 additions & 5 deletions tests/common/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2532,6 +2532,16 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<
pub fn run_ucmd_as_root(
ts: &TestScenario,
args: &[&str],
) -> std::result::Result<CmdResult, String> {
run_ucmd_as_root_with_stdin_stdout(ts, args, None, None)
}

#[cfg(unix)]
pub fn run_ucmd_as_root_with_stdin_stdout(
ts: &TestScenario,
args: &[&str],
stdin: Option<&str>,
stdout: Option<&str>,
) -> std::result::Result<CmdResult, String> {
if is_ci() {
Err(format!("{UUTILS_INFO}: {}", "cannot run inside CI"))
Expand All @@ -2546,16 +2556,21 @@ pub fn run_ucmd_as_root(
Ok(output) if String::from_utf8_lossy(&output.stdout).eq("root\n") => {
// we can run sudo and we're root
// run ucmd as root:
Ok(ts
.cmd("sudo")
.env("PATH", PATH)
let mut cmd = ts.cmd("sudo");
cmd.env("PATH", PATH)
.envs(DEFAULT_ENV)
.arg("-E")
.arg("--non-interactive")
.arg(&ts.bin_path)
.arg(&ts.util_name)
.args(args)
.run())
.args(args);
if let Some(stdin) = stdin {
cmd.set_stdin(File::open(stdin).unwrap());
}
if let Some(stdout) = stdout {
cmd.set_stdout(File::open(stdout).unwrap());
}
Ok(cmd.run())
}
Ok(output)
if String::from_utf8_lossy(&output.stderr).eq("sudo: a password is required\n") =>
Expand Down