From 8cf54af8f885324720c0949126e757710402a85e Mon Sep 17 00:00:00 2001 From: FidelSch Date: Tue, 9 Dec 2025 16:02:40 -0300 Subject: [PATCH 01/11] tac: mimic GNU on handling special files such as /dev/zero --- src/uu/tac/src/tac.rs | 27 +++++++++++++-------------- tests/by-util/test_tac.rs | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 507dd153199..68eb106dcd0 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -11,10 +11,7 @@ use memchr::memmem; use memmap2::Mmap; use std::ffi::OsString; use std::io::{BufWriter, Read, Write, stdin, stdout}; -use std::{ - fs::{File, read}, - path::Path, -}; +use std::{fs::File, path::Path}; use uucore::error::UError; use uucore::error::UResult; use uucore::{format_usage, show}; @@ -268,17 +265,14 @@ 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 buf1 = Vec::new(); + if let Err(e) = File::open(path).and_then(|mut f| f.read_to_end(&mut buf1)) { + let e: Box = TacError::ReadError(filename.clone(), e).into(); + show!(e); + continue; } + buf = buf1; + &buf } }; @@ -306,6 +300,11 @@ fn try_mmap_stdin() -> Option { 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()? }; diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 0f5aad48808..8413cbf6e44 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -335,3 +335,20 @@ fn test_failed_write_is_reported() { .fails() .stderr_is("tac: failed to write to stdout: No space left on device (os error 28)\n"); } + +#[cfg(target_os = "linux")] +#[test] +fn test_infinite_pipe() { + use std::time::Duration; + + let mut child = new_ucmd!().arg("/dev/zero").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(); +} From cab20c51f08bf81f73e3dc3b1092043aafc6d40f Mon Sep 17 00:00:00 2001 From: FidelSch Date: Tue, 9 Dec 2025 16:02:40 -0300 Subject: [PATCH 02/11] tac: mimic GNU on handling special files such as /dev/zero --- src/uu/tac/src/tac.rs | 27 +++++++++++++-------------- tests/by-util/test_tac.rs | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 507dd153199..68eb106dcd0 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -11,10 +11,7 @@ use memchr::memmem; use memmap2::Mmap; use std::ffi::OsString; use std::io::{BufWriter, Read, Write, stdin, stdout}; -use std::{ - fs::{File, read}, - path::Path, -}; +use std::{fs::File, path::Path}; use uucore::error::UError; use uucore::error::UResult; use uucore::{format_usage, show}; @@ -268,17 +265,14 @@ 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 buf1 = Vec::new(); + if let Err(e) = File::open(path).and_then(|mut f| f.read_to_end(&mut buf1)) { + let e: Box = TacError::ReadError(filename.clone(), e).into(); + show!(e); + continue; } + buf = buf1; + &buf } }; @@ -306,6 +300,11 @@ fn try_mmap_stdin() -> Option { 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()? }; diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 0f5aad48808..6df00f0db96 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -335,3 +335,23 @@ fn test_failed_write_is_reported() { .fails() .stderr_is("tac: failed to write to stdout: No space left on device (os error 28)\n"); } + +#[cfg(target_os = "linux")] +#[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(); +} From 39371c443ad252f6807c77d3445499cca2c7ce55 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 18 Dec 2025 13:26:00 -0300 Subject: [PATCH 03/11] tac: improve assertion message for infinite pipe test --- tests/by-util/test_tac.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 6df00f0db96..531f241ff99 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -350,7 +350,13 @@ fn test_infinite_pipe() { std::thread::sleep(Duration::from_secs(5)); // The process should not have exited, as the stream is infinite - assert!(child.is_alive()); + assert!( + child.is_alive(), + // This might actually happen on 32-bit systems due to memory constraints + // TODO: Address this properly + "tac exited unexpectedly when reading from an infinite stream, error: {:?}", + child.wait().ok() + ); // Clean up child.kill(); From a83cedda4f09a5beafb7e9f3976a9de95f03611b Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 18 Dec 2025 15:17:29 -0300 Subject: [PATCH 04/11] test(tac): limit infinite pipe test to 64-bit targets only --- tests/by-util/test_tac.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 531f241ff99..453e34e7861 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -336,7 +336,10 @@ fn test_failed_write_is_reported() { .stderr_is("tac: failed to write to stdout: No space left on device (os error 28)\n"); } -#[cfg(target_os = "linux")] +// 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}; @@ -350,13 +353,7 @@ fn test_infinite_pipe() { std::thread::sleep(Duration::from_secs(5)); // The process should not have exited, as the stream is infinite - assert!( - child.is_alive(), - // This might actually happen on 32-bit systems due to memory constraints - // TODO: Address this properly - "tac exited unexpectedly when reading from an infinite stream, error: {:?}", - child.wait().ok() - ); + assert!(child.is_alive()); // Clean up child.kill(); From 40381d7c8dee775a65cf361d078c01783d51025d Mon Sep 17 00:00:00 2001 From: FidelSch Date: Tue, 23 Dec 2025 17:56:21 -0300 Subject: [PATCH 05/11] tac: Eliminate unnecesary memory use when reading infinite streams --- src/uu/tac/src/tac.rs | 58 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 68eb106dcd0..99359bfad81 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -10,7 +10,7 @@ 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::io::{BufWriter, Read, Seek, Write, stdin, stdout}; use std::{fs::File, path::Path}; use uucore::error::UError; use uucore::error::UResult; @@ -265,8 +265,17 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR mmap = mmap1; &mmap } else { - let mut buf1 = Vec::new(); - if let Err(e) = File::open(path).and_then(|mut f| f.read_to_end(&mut buf1)) { + 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; @@ -311,3 +320,46 @@ 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]; + 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 +} \ No newline at end of file From 8c23f6120c654c84c348de5e8552a050bdbf734e Mon Sep 17 00:00:00 2001 From: FidelSch Date: Tue, 23 Dec 2025 18:04:51 -0300 Subject: [PATCH 06/11] tac: formatting --- src/uu/tac/src/tac.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 99359bfad81..13f1aa40449 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -322,10 +322,10 @@ fn try_mmap_path(path: &Path) -> Option { } /// Attempt to seek to end of file -/// -/// Returns `Some(size)` if successful, `None` if unable to determine size. +/// +/// 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(); @@ -346,7 +346,7 @@ fn try_seek_end(file: &mut File) -> Option { loop { let mut byte = [0u8; 1]; match file.read(&mut byte) { - Ok(0) => break, // Found EOF + Ok(0) => break, // Found EOF Ok(_) => continue, // Keep looking Err(_) => break, } @@ -362,4 +362,4 @@ fn try_seek_end(file: &mut File) -> Option { file.seek(std::io::SeekFrom::Start(0)).ok()?; size -} \ No newline at end of file +} From e1544e07d2bc09d4cdf9396255b489495103cf73 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Mon, 2 Feb 2026 17:09:19 -0300 Subject: [PATCH 07/11] tac: make clippy happy --- src/uu/tac/src/tac.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 9ff175b0b54..f5d29c6be60 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -565,8 +565,8 @@ fn try_seek_end(file: &mut File) -> Option { loop { let mut byte = [0u8; 1]; match file.read(&mut byte) { - Ok(0) => break, // Found EOF - Ok(_) => continue, // Keep looking + Ok(0) => break, // Found EOF + Ok(_) => {} // Keep looking Err(_) => break, } } From ba8396667841e31cb7b74d893f898d62e9c29210 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Mon, 2 Feb 2026 17:09:19 -0300 Subject: [PATCH 08/11] tac: make clippy happy --- src/uu/tac/src/tac.rs | 86 +++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 9ff175b0b54..b117723d800 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -455,6 +455,49 @@ 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]; + 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(_) => {} // 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 +} + #[cfg(test)] mod tests_hybrid_flavor { use super::translate_regex_flavor; @@ -539,46 +582,3 @@ mod tests_hybrid_flavor { assert_eq!(translate_regex_flavor(r"\^"), r"\^"); } } - -/// 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]; - 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 -} From addcd0e297cc12e3e3df3d1c8088fdb75334190b Mon Sep 17 00:00:00 2001 From: FidelSch Date: Wed, 11 Feb 2026 09:56:38 -0300 Subject: [PATCH 09/11] tac: refactor try_seek_end --- src/uu/tac/src/tac.rs | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index f95b6c28b6d..a7a922b4417 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -473,26 +473,24 @@ fn try_seek_end(file: &mut File) -> Option { // 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(_) => {} // Keep looking - Err(_) => break, - } - } - // TODO: Prove this is actually unreachable - unreachable!(); + if file.read(&mut test_byte).ok()? == 0 { + // Truly empty file + return size; + } else { + // 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(_) => {} // Keep looking + Err(_) => break, + } } + + // TODO: Prove this is actually unreachable + unreachable!(); } } From 13fef2d1d9817fec314cd615c14b52a64abf85c9 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Wed, 11 Feb 2026 10:03:27 -0300 Subject: [PATCH 10/11] tac: Fix handling of zero-size files in try_seek_end Actually try to read until EOF for zero-size files, instead of assuming they are either empty or infinite. --- src/uu/tac/src/tac.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index a7a922b4417..99db05d0f4f 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -479,18 +479,18 @@ fn try_seek_end(file: &mut File) -> Option { return size; } else { // Has data despite size 0 - likely a pipe or special file - // Loop forever looking for EOF + // 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(_) => {} // Keep looking - Err(_) => break, + Ok(0) => break, // Found EOF + Ok(n) => read_size += n, // Keep looking + Err(_) => return None, // Error reading - give up on seeking } } - // TODO: Prove this is actually unreachable - unreachable!(); + return Some(read_size as u64); } } From cc0a1a2f128dd5aa44fc508826065e446e75c9fd Mon Sep 17 00:00:00 2001 From: FidelSch Date: Wed, 11 Feb 2026 11:37:10 -0300 Subject: [PATCH 11/11] lint(tac): make clippy hapy py --- src/uu/tac/src/tac.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 99db05d0f4f..296b5a89944 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -477,21 +477,21 @@ fn try_seek_end(file: &mut File) -> Option { if file.read(&mut test_byte).ok()? == 0 { // Truly empty file return size; - } else { - // 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); + // 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