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
82 changes: 66 additions & 16 deletions src/uu/tac/src/tac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,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};
use uucore::error::UError;
use uucore::error::UResult;
use uucore::{format_usage, show};
Expand Down Expand Up @@ -278,17 +274,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<dyn UError> = 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<dyn UError> = TacError::ReadError(filename.clone(), e).into();
show!(e);
continue;
}
buf = buf1;
&buf
}
};

Expand Down Expand Up @@ -340,9 +342,57 @@ fn buffer_stdin() -> std::io::Result<StdinData> {
fn try_mmap_path(path: &Path) -> Option<Mmap> {
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()? };

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<u64> {
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];
match file.read(&mut test_byte).ok()? {
0 => {
// Truly empty file
return size;
}
_ => {
// Has data despite size 0 - likely a pipe or special file
// Loop forever looking for EOF
loop {
let mut byte = [0u8; 1];
match file.read(&mut byte) {
Ok(0) => break, // Found EOF
Ok(_) => continue, // Keep looking
Err(_) => break,
}
}

// TODO: Prove this is actually unreachable
unreachable!();
}
}
}

// Leave the file cursor at the start
file.seek(std::io::SeekFrom::Start(0)).ok()?;

size
}
23 changes: 23 additions & 0 deletions tests/by-util/test_tac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,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() {
Expand Down
Loading