diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 164cd58aa5b..296b5a89944 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -11,12 +11,8 @@ use clap::{Arg, ArgAction, Command}; use memchr::memmem; use memmap2::Mmap; use std::ffi::OsString; -use std::io::{BufWriter, Read, Write, stdin, stdout}; -use std::{ - fs::{File, read}, - io::copy, - path::Path, -}; +use std::io::{BufWriter, Read, Seek, Write, copy, stdin, stdout}; +use std::{fs::File, path::Path}; #[cfg(unix)] use uucore::error::set_exit_code; use uucore::error::{UError, UResult}; @@ -379,17 +375,23 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR mmap = mmap1; &mmap } else { - match read(path) { - Ok(buf1) => { - buf = buf1; - &buf - } - Err(e) => { - let e: Box = TacError::ReadError(filename.clone(), e).into(); - show!(e); - continue; - } + let mut f = File::open(path)?; + let mut buf1; + + if let Some(size) = try_seek_end(&mut f) { + // Normal file with known size + buf1 = Vec::with_capacity(size as usize); + } else { + // Unable to determine size - fall back to normal read + buf1 = Vec::new(); } + if let Err(e) = f.read_to_end(&mut buf1) { + let e: Box = TacError::ReadError(filename.clone(), e).into(); + show!(e); + continue; + } + buf = buf1; + &buf } }; @@ -445,6 +447,11 @@ fn buffer_stdin() -> std::io::Result { fn try_mmap_path(path: &Path) -> Option { let file = File::open(path).ok()?; + // Only mmap regular files. + if !file.metadata().ok()?.is_file() { + return None; + } + // SAFETY: If the file is truncated while we map it, SIGBUS will be raised // and our process will be terminated, thus preventing access of invalid memory. let mmap = unsafe { Mmap::map(&file).ok()? }; @@ -452,6 +459,47 @@ fn try_mmap_path(path: &Path) -> Option { Some(mmap) } +/// Attempt to seek to end of file +/// +/// Returns `Some(size)` if successful, `None` if unable to determine size. +/// Hangs if file is an infinite stream. +/// +/// Leaves file cursor at start of file +fn try_seek_end(file: &mut File) -> Option { + let size = file.seek(std::io::SeekFrom::End(0)).ok(); + + if size == Some(0) { + // Might be an empty file or infinite stream; + // Try reading a byte to distinguish + file.seek(std::io::SeekFrom::Start(0)).ok()?; + let mut test_byte = [0u8; 1]; + + if file.read(&mut test_byte).ok()? == 0 { + // Truly empty file + return size; + } + + // Has data despite size 0 - likely a pipe or special file + // Loop looking for EOF + let mut read_size = 1; + loop { + let mut byte = [0u8; 1]; + match file.read(&mut byte) { + Ok(0) => break, // Found EOF + Ok(n) => read_size += n, // Keep looking + Err(_) => return None, // Error reading - give up on seeking + } + } + + return Some(read_size as u64); + } + + // Leave the file cursor at the start + file.seek(std::io::SeekFrom::Start(0)).ok()?; + + size +} + #[cfg(test)] mod tests_hybrid_flavor { use super::translate_regex_flavor; diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index dee01967749..dce96794a13 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -309,6 +309,29 @@ fn test_failed_write_is_reported() { .stderr_is("tac: failed to write to stdout: No space left on device (os error 28)\n"); } +// Test that `tac` can handle an infinite input stream without exiting. +// Only run on 64-bit systems, as on 32-bit systems, +// `tac` may run out of memory when trying to buffer the infinite input. +#[cfg(all(target_os = "linux", target_pointer_width = "64"))] +#[test] +fn test_infinite_pipe() { + use std::{fs::File, time::Duration}; + + let mut child = new_ucmd!() + .arg("/dev/zero") + .set_stdout(File::open("/dev/null").unwrap()) + .run_no_wait(); + + // Wait for a while + std::thread::sleep(Duration::from_secs(5)); + + // The process should not have exited, as the stream is infinite + assert!(child.is_alive()); + + // Clean up + child.kill(); +} + #[cfg(target_os = "linux")] #[test] fn test_stdin_bad_tmpdir_fallback() {