Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8cf54af
tac: mimic GNU on handling special files such as /dev/zero
FidelSch Dec 9, 2025
cab20c5
tac: mimic GNU on handling special files such as /dev/zero
FidelSch Dec 9, 2025
9469998
Merge branch 'tac-special-files' of https://github.com/FidelSch/coreu…
FidelSch Dec 10, 2025
39371c4
tac: improve assertion message for infinite pipe test
FidelSch Dec 18, 2025
a83cedd
test(tac): limit infinite pipe test to 64-bit targets only
FidelSch Dec 18, 2025
b5a7ed3
Merge branch 'uutils:main' into tac-special-files
FidelSch Dec 23, 2025
40381d7
tac: Eliminate unnecesary memory use when reading infinite streams
FidelSch Dec 23, 2025
8c23f61
tac: formatting
FidelSch Dec 23, 2025
d677270
Merge branch 'main' into tac-special-files
sylvestre Dec 29, 2025
9e407a3
Merge branch 'main' into tac-special-files
FidelSch Jan 16, 2026
19c62a3
Merge branch 'main' into tac-special-files
FidelSch Jan 16, 2026
c9ebd1b
Merge branch 'tac-special-files' of https://github.com/FidelSch/coreu…
FidelSch Jan 16, 2026
31e84fe
Merge https://github.com/uutils/coreutils into tac-special-files
FidelSch Feb 2, 2026
e1544e0
tac: make clippy happy
FidelSch Feb 2, 2026
ba83966
tac: make clippy happy
FidelSch Feb 2, 2026
9f40e98
Merge branch 'tac-special-files' of https://github.com/FidelSch/coreu…
FidelSch Feb 3, 2026
d29d523
Merge branch 'tac-special-files' of https://github.com/FidelSch/coreu…
FidelSch Feb 3, 2026
1c68b18
Merge branch 'tac-special-files' of https://github.com/FidelSch/coreu…
FidelSch Feb 3, 2026
a0c0b74
Merge branch 'main' into tac-special-files
sylvestre Feb 10, 2026
addcd0e
tac: refactor try_seek_end
FidelSch Feb 11, 2026
13fef2d
tac: Fix handling of zero-size files in try_seek_end
FidelSch Feb 11, 2026
cc0a1a2
lint(tac): make clippy hapy
FidelSch Feb 11, 2026
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
80 changes: 64 additions & 16 deletions src/uu/tac/src/tac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<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 @@ -445,13 +447,59 @@ 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];

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