diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 896e0c923e1..46422c7fe18 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -193,7 +193,16 @@ fn print_n_bytes(input: impl Read, n: u64) -> io::Result { Ok(bytes_written) } -fn print_n_lines(input: &mut impl io::BufRead, n: u64, separator: u8) -> io::Result { +enum HeadFileError { + Read(io::Error), + WriteStdout(io::Error), +} + +fn print_n_lines( + input: &mut impl io::BufRead, + n: u64, + separator: u8, +) -> Result { // Read the first `n` lines from the `input` reader. let mut reader = take_lines(input, n, separator); @@ -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) } @@ -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 { +fn head_file(input: &mut File, options: &HeadOptions) -> Result { 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) + } } } @@ -424,10 +453,30 @@ 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()), + } } } @@ -435,14 +484,31 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { { 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(()) @@ -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 { diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 8406c3d9082..e6149dd18d8 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -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 @@ -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")]