1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +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 clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use std::ffi::OsString; use std::ffi::OsString;
use std::fs::Metadata;
use std::io::{self, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write}; 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::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError}; use uucore::error::{FromIo, UResult, USimpleError};
use uucore::line_ending::LineEnding; 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(()) 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 { if n == 0 {
//prints everything //prints everything
return read_n_bytes(input, std::u64::MAX); return read_n_bytes(input, std::u64::MAX);
} }
let stdout = std::io::stdout(); if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
let mut stdout = stdout.lock(); 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 buffer = [0u8; BUF_SIZE];
let mut total_read = 0; let mut total_read = 0;
loop { loop {
let read = match input.read(&mut buffer) { let read = match input.read(&mut buffer) {
Ok(0) => break, Ok(0) => break,
Ok(read) => read, Ok(read) => read,
Err(e) => match e.kind() { Err(e) => match e.kind() {
ErrorKind::Interrupted => continue, ErrorKind::Interrupted => continue,
_ => return Err(e), _ => return Err(e),
}, },
}; };
total_read += read; total_read += read;
if total_read <= n { if total_read <= n {
// Fill the ring buffer without exceeding n bytes // Fill the ring buffer without exceeding n bytes
let overflow = total_read - n; let overflow = total_read - n;
ring_buffer.extend_from_slice(&buffer[..read - overflow]); ring_buffer.extend_from_slice(&buffer[..read - overflow]);
} else { } else {
// Write the ring buffer and the part of the buffer that exceeds n // Write the ring buffer and the part of the buffer that exceeds n
stdout.write_all(&ring_buffer)?; stdout.write_all(&ring_buffer)?;
stdout.write_all(&buffer[..read - n + ring_buffer.len()])?; stdout.write_all(&buffer[..read - n + ring_buffer.len()])?;
ring_buffer.clear(); ring_buffer.clear();
ring_buffer.extend_from_slice(&buffer[read - n + ring_buffer.len()..read]); 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( fn read_but_last_n_lines(
input: impl std::io::BufRead, input: impl std::io::BufRead,
n: usize, n: u64,
separator: u8, separator: u8,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let stdout = std::io::stdout(); if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
let mut stdout = stdout.lock(); let stdout = std::io::stdout();
for bytes in take_all_but(lines(input, separator), n) { let mut stdout = stdout.lock();
stdout.write_all(&bytes?)?; for bytes in take_all_but(lines(input, separator), n) {
stdout.write_all(&bytes?)?;
}
} }
Ok(()) 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<()> { 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 { match options.mode {
Mode::AllButLastBytes(n) => { Mode::AllButLastBytes(n) => {
let size = input.metadata()?.len(); let size = input.metadata()?.len();
@ -428,32 +504,13 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
let stdin = std::io::stdin(); let stdin = std::io::stdin();
let mut stdin = stdin.lock(); 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 { match options.mode {
Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n), 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),
Mode::AllButLastBytes(n) => {
read_but_last_n_bytes(&mut stdin, n.try_into().unwrap())
}
Mode::FirstLines(n) => read_n_lines(&mut stdin, n, options.line_ending.into()), 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) => {
Mode::AllButLastLines(n) => read_but_last_n_lines( read_but_last_n_lines(&mut stdin, n, options.line_ending.into())
&mut stdin, }
n.try_into().unwrap(),
options.line_ending.into(),
),
} }
} }
(name, false) => { (name, false) => {

View file

@ -330,7 +330,7 @@ fn test_head_invalid_num() {
new_ucmd!() new_ucmd!()
.args(&["-c", size]) .args(&["-c", size])
.fails() .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!() new_ucmd!()
@ -378,3 +378,61 @@ fn test_presume_input_pipe_5_chars() {
.run() .run()
.stdout_is_fixture("lorem_ipsum_5_chars.expected"); .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");
}