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
102 changes: 89 additions & 13 deletions src/uu/head/src/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,16 @@ fn print_n_bytes(input: impl Read, n: u64) -> io::Result<u64> {
Ok(bytes_written)
}

fn print_n_lines(input: &mut impl io::BufRead, n: u64, separator: u8) -> io::Result<u64> {
enum HeadFileError {
Read(io::Error),
WriteStdout(io::Error),
}

fn print_n_lines(
input: &mut impl io::BufRead,
n: u64,
separator: u8,
) -> Result<u64, HeadFileError> {
// Read the first `n` lines from the `input` reader.
let mut reader = take_lines(input, n, separator);

Expand All @@ -202,12 +211,30 @@ fn print_n_lines(input: &mut impl io::BufRead, n: u64, separator: u8) -> io::Res
let stdout = stdout.lock();
let mut writer = BufWriter::with_capacity(BUF_SIZE, stdout);

let bytes_written = io::copy(&mut reader, &mut writer).map_err(wrap_in_stdout_error)?;
let mut bytes_written = 0;
let mut buf = [0; BUF_SIZE];
loop {
let bytes_read = reader.read(&mut buf).map_err(HeadFileError::Read)?;

if bytes_read == 0 {
break;
}

writer
.write_all(&buf[..bytes_read])
.map_err(wrap_in_stdout_error)
.map_err(HeadFileError::WriteStdout)?;

bytes_written += bytes_read as u64;
}

// Make sure we finish writing everything to the target before
// exiting. Otherwise, when Rust is implicitly flushing, any
// error will be silently ignored.
writer.flush().map_err(wrap_in_stdout_error)?;
writer
.flush()
.map_err(wrap_in_stdout_error)
.map_err(HeadFileError::WriteStdout)?;

Ok(bytes_written)
}
Expand Down Expand Up @@ -388,15 +415,17 @@ fn head_backwards_on_seekable_file(input: &mut File, options: &HeadOptions) -> i
}
}

fn head_file(input: &mut File, options: &HeadOptions) -> io::Result<u64> {
fn head_file(input: &mut File, options: &HeadOptions) -> Result<u64, HeadFileError> {
match options.mode {
Mode::FirstBytes(n) => print_n_bytes(input, n),
Mode::FirstBytes(n) => print_n_bytes(input, n).map_err(HeadFileError::WriteStdout),
Mode::FirstLines(n) => print_n_lines(
&mut io::BufReader::with_capacity(BUF_SIZE, input),
n,
options.line_ending.into(),
),
Mode::AllButLastBytes(_) | Mode::AllButLastLines(_) => head_backwards_file(input, options),
Mode::AllButLastBytes(_) | Mode::AllButLastLines(_) => {
head_backwards_file(input, options).map_err(HeadFileError::WriteStdout)
}
}
}

Expand Down Expand Up @@ -424,25 +453,62 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
// last byte read so that any tools that parse the remainder of
// the stdin stream read from the correct place.

let bytes_read = head_file(&mut stdin_file, options)?;
let bytes_read = match head_file(&mut stdin_file, options) {
Ok(bytes_read) => bytes_read,
Err(HeadFileError::Read(err)) => {
return Err(HeadError::Io {
name: "standard input".into(),
err,
}
.into());
}
Err(HeadFileError::WriteStdout(err)) => return Err(err.into()),
};
stdin_file.seek(SeekFrom::Start(current_pos + bytes_read))?;
} else {
let _bytes_read = head_file(&mut stdin_file, options)?;
match head_file(&mut stdin_file, options) {
Ok(_) => {}
Err(HeadFileError::Read(err)) => {
return Err(HeadError::Io {
name: "standard input".into(),
err,
}
.into());
}
Err(HeadFileError::WriteStdout(err)) => return Err(err.into()),
}
}
}

#[cfg(not(unix))]
{
let mut stdin = stdin.lock();

match options.mode {
Mode::FirstBytes(n) => print_n_bytes(&mut stdin, n),
Mode::AllButLastBytes(n) => print_but_last_n_bytes(&mut stdin, n),
let result = match options.mode {
Mode::FirstBytes(n) => {
print_n_bytes(&mut stdin, n).map_err(HeadFileError::WriteStdout)
}
Mode::AllButLastBytes(n) => {
print_but_last_n_bytes(&mut stdin, n).map_err(HeadFileError::WriteStdout)
}
Mode::FirstLines(n) => print_n_lines(&mut stdin, n, options.line_ending.into()),
Mode::AllButLastLines(n) => {
print_but_last_n_lines(&mut stdin, n, options.line_ending.into())
.map_err(HeadFileError::WriteStdout)
}
};

match result {
Ok(_) => {}
Err(HeadFileError::Read(err)) => {
return Err(HeadError::Io {
name: "standard input".into(),
err,
}
.into());
}
}?;
Err(HeadFileError::WriteStdout(err)) => return Err(err.into()),
}
}

Ok(())
Expand Down Expand Up @@ -493,7 +559,17 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
continue;
}
};
head_file(&mut file_handle, options)?;
match head_file(&mut file_handle, options) {
Ok(_) => {}
Err(HeadFileError::Read(err)) => {
show!(HeadError::Io {
name: file.into(),
err
});
continue;
}
Err(HeadFileError::WriteStdout(err)) => return Err(err.into()),
}
Ok(())
};
if let Err(err) = res {
Expand Down
35 changes: 35 additions & 0 deletions tests/by-util/test_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,22 @@ fn test_multiple_nonexistent_files() {
.stderr_contains("cannot open 'bogusfile2' for reading: No such file or directory");
}

#[test]
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
fn test_multiple_files_read_error_continues_to_next_file() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

at.write("a", "hello\n");

ts.ucmd()
.args(&["/proc/self/mem", "a"])
.fails()
.stdout_is("==> /proc/self/mem <==\n\n==> a <==\nhello\n")
.stderr_contains("head: error reading '/proc/self/mem': Input/output error");
}

// there was a bug not caught by previous tests
// where for negative n > 3, the total amount of lines
// was correct, but it would eat from the second line
Expand Down Expand Up @@ -901,6 +917,25 @@ fn test_write_to_dev_full() {
}
}

#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
#[test]
fn test_write_to_dev_full_with_named_file() {
use std::fs::OpenOptions;

let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

at.write("input", "hello\nworld\n");

let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();

ts.ucmd()
.arg("input")
.set_stdout(dev_full)
.fails()
.stderr_is("head: error writing 'standard output': No space left on device\n");
}

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")]
Expand Down
Loading