1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

head: head_backwards for non-seekable files like /proc/* or fifos (named pipes) (#5732)

* implement head_backwards for non-seekable files like /proc/* or pipes

Signed-off-by: Ulrich Hornung <hornunguli@gmx.de>
This commit is contained in:
cre4ture 2024-01-05 00:25:59 +01:00 committed by GitHub
parent be816027ae
commit 9b3cc5437c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 170 additions and 55 deletions

View file

@ -7,7 +7,10 @@
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use std::ffi::OsString;
use std::fs::Metadata;
use std::io::{self, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write};
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::MetadataExt;
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError};
use uucore::line_ending::LineEnding;
@ -243,42 +246,57 @@ fn read_n_lines(input: &mut impl std::io::BufRead, n: u64, separator: u8) -> std
Ok(())
}
fn read_but_last_n_bytes(input: &mut impl std::io::BufRead, n: usize) -> std::io::Result<()> {
fn catch_too_large_numbers_in_backwards_bytes_or_lines(n: u64) -> Option<usize> {
match usize::try_from(n) {
Ok(value) => Some(value),
Err(e) => {
show!(USimpleError::new(
1,
format!("{e}: number of -bytes or -lines is too large")
));
None
}
}
}
fn read_but_last_n_bytes(input: &mut impl std::io::BufRead, n: u64) -> std::io::Result<()> {
if n == 0 {
//prints everything
return read_n_bytes(input, std::u64::MAX);
}
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let mut ring_buffer = Vec::new();
let mut ring_buffer = Vec::new();
let mut buffer = [0u8; BUF_SIZE];
let mut total_read = 0;
let mut buffer = [0u8; BUF_SIZE];
let mut total_read = 0;
loop {
let read = match input.read(&mut buffer) {
Ok(0) => break,
Ok(read) => read,
Err(e) => match e.kind() {
ErrorKind::Interrupted => continue,
_ => return Err(e),
},
};
loop {
let read = match input.read(&mut buffer) {
Ok(0) => break,
Ok(read) => read,
Err(e) => match e.kind() {
ErrorKind::Interrupted => continue,
_ => return Err(e),
},
};
total_read += read;
total_read += read;
if total_read <= n {
// Fill the ring buffer without exceeding n bytes
let overflow = total_read - n;
ring_buffer.extend_from_slice(&buffer[..read - overflow]);
} else {
// Write the ring buffer and the part of the buffer that exceeds n
stdout.write_all(&ring_buffer)?;
stdout.write_all(&buffer[..read - n + ring_buffer.len()])?;
ring_buffer.clear();
ring_buffer.extend_from_slice(&buffer[read - n + ring_buffer.len()..read]);
if total_read <= n {
// Fill the ring buffer without exceeding n bytes
let overflow = total_read - n;
ring_buffer.extend_from_slice(&buffer[..read - overflow]);
} else {
// Write the ring buffer and the part of the buffer that exceeds n
stdout.write_all(&ring_buffer)?;
stdout.write_all(&buffer[..read - n + ring_buffer.len()])?;
ring_buffer.clear();
ring_buffer.extend_from_slice(&buffer[read - n + ring_buffer.len()..read]);
}
}
}
@ -287,13 +305,15 @@ fn read_but_last_n_bytes(input: &mut impl std::io::BufRead, n: usize) -> std::io
fn read_but_last_n_lines(
input: impl std::io::BufRead,
n: usize,
n: u64,
separator: u8,
) -> std::io::Result<()> {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
for bytes in take_all_but(lines(input, separator), n) {
stdout.write_all(&bytes?)?;
if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
for bytes in take_all_but(lines(input, separator), n) {
stdout.write_all(&bytes?)?;
}
}
Ok(())
}
@ -374,7 +394,63 @@ where
}
}
fn is_seekable(input: &mut std::fs::File) -> bool {
let current_pos = input.stream_position();
current_pos.is_ok()
&& input.seek(SeekFrom::End(0)).is_ok()
&& input.seek(SeekFrom::Start(current_pos.unwrap())).is_ok()
}
fn sanity_limited_blksize(_st: &Metadata) -> u64 {
#[cfg(not(target_os = "windows"))]
{
const DEFAULT: u64 = 512;
const MAX: u64 = usize::MAX as u64 / 8 + 1;
let st_blksize: u64 = _st.blksize();
match st_blksize {
0 => DEFAULT,
1..=MAX => st_blksize,
_ => DEFAULT,
}
}
#[cfg(target_os = "windows")]
{
512
}
}
fn head_backwards_file(input: &mut std::fs::File, options: &HeadOptions) -> std::io::Result<()> {
let st = input.metadata()?;
let seekable = is_seekable(input);
let blksize_limit = sanity_limited_blksize(&st);
if !seekable || st.len() <= blksize_limit {
return head_backwards_without_seek_file(input, options);
}
head_backwards_on_seekable_file(input, options)
}
fn head_backwards_without_seek_file(
input: &mut std::fs::File,
options: &HeadOptions,
) -> std::io::Result<()> {
let reader = &mut std::io::BufReader::with_capacity(BUF_SIZE, &*input);
match options.mode {
Mode::AllButLastBytes(n) => read_but_last_n_bytes(reader, n)?,
Mode::AllButLastLines(n) => read_but_last_n_lines(reader, n, options.line_ending.into())?,
_ => unreachable!(),
}
Ok(())
}
fn head_backwards_on_seekable_file(
input: &mut std::fs::File,
options: &HeadOptions,
) -> std::io::Result<()> {
match options.mode {
Mode::AllButLastBytes(n) => {
let size = input.metadata()?.len();
@ -428,32 +504,13 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
let stdin = std::io::stdin();
let mut stdin = stdin.lock();
// Outputting "all-but-last" requires us to use a ring buffer with size n, so n
// must be converted from u64 to usize to fit in memory. If such conversion fails,
// it means the platform doesn't have enough memory to hold the buffer, so we fail.
if let Mode::AllButLastLines(n) | Mode::AllButLastBytes(n) = options.mode {
if let Err(e) = usize::try_from(n) {
show!(USimpleError::new(
1,
format!("{e}: number of bytes is too large")
));
continue;
};
};
match options.mode {
Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n),
// unwrap is guaranteed to succeed because we checked the value of n above
Mode::AllButLastBytes(n) => {
read_but_last_n_bytes(&mut stdin, n.try_into().unwrap())
}
Mode::AllButLastBytes(n) => read_but_last_n_bytes(&mut stdin, n),
Mode::FirstLines(n) => read_n_lines(&mut stdin, n, options.line_ending.into()),
// unwrap is guaranteed to succeed because we checked the value of n above
Mode::AllButLastLines(n) => read_but_last_n_lines(
&mut stdin,
n.try_into().unwrap(),
options.line_ending.into(),
),
Mode::AllButLastLines(n) => {
read_but_last_n_lines(&mut stdin, n, options.line_ending.into())
}
}
}
(name, false) => {

View file

@ -330,7 +330,7 @@ fn test_head_invalid_num() {
new_ucmd!()
.args(&["-c", size])
.fails()
.stderr_is("head: out of range integral type conversion attempted: number of bytes is too large\n");
.stderr_is("head: out of range integral type conversion attempted: number of -bytes or -lines is too large\n");
}
}
new_ucmd!()
@ -378,3 +378,61 @@ fn test_presume_input_pipe_5_chars() {
.run()
.stdout_is_fixture("lorem_ipsum_5_chars.expected");
}
#[cfg(all(
not(target_os = "windows"),
not(target_os = "macos"),
not(target_os = "freebsd")
))]
#[test]
fn test_read_backwards_bytes_proc_fs_version() {
let ts = TestScenario::new(util_name!());
let args = ["-c", "-1", "/proc/version"];
let result = ts.ucmd().args(&args).succeeds();
assert!(result.stdout().len() > 0);
}
#[cfg(all(
not(target_os = "windows"),
not(target_os = "macos"),
not(target_os = "freebsd")
))]
#[test]
fn test_read_backwards_bytes_proc_fs_modules() {
let ts = TestScenario::new(util_name!());
let args = ["-c", "-1", "/proc/modules"];
let result = ts.ucmd().args(&args).succeeds();
assert!(result.stdout().len() > 0);
}
#[cfg(all(
not(target_os = "windows"),
not(target_os = "macos"),
not(target_os = "freebsd")
))]
#[test]
fn test_read_backwards_lines_proc_fs_modules() {
let ts = TestScenario::new(util_name!());
let args = ["--lines", "-1", "/proc/modules"];
let result = ts.ucmd().args(&args).succeeds();
assert!(result.stdout().len() > 0);
}
#[cfg(all(
not(target_os = "windows"),
not(target_os = "macos"),
not(target_os = "freebsd")
))]
#[test]
fn test_read_backwards_bytes_sys_kernel_profiling() {
let ts = TestScenario::new(util_name!());
let args = ["-c", "-1", "/sys/kernel/profiling"];
let result = ts.ucmd().args(&args).succeeds();
let stdout_str = result.stdout_str();
assert_eq!(stdout_str.len(), 1);
assert!(stdout_str == "0" || stdout_str == "1");
}