From 51fba8976e17f1b75602394bcf437ddbbe5d8dd6 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Fri, 30 Oct 2026 06:02:29 +0800 Subject: [PATCH 1/2] head: honor the current stdin offset for -n -N and -c -N --- src/uu/head/src/head.rs | 43 ++++++++++++++++++++------- tests/by-util/test_head.rs | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 8164d28f769..ad4bf1b53f2 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -331,19 +331,23 @@ fn find_nth_line_from_end(input: &mut R, n: u64, separator: u8) -> io::Result where R: Read + Seek, { + // Only search the data after the current position, so a partly consumed + // seekable input (such as a regular file used as stdin) is handled + // relative to where the descriptor currently stands. + let start_offset = input.stream_position()?; let file_size = input.seek(SeekFrom::End(0))?; let mut buffer = [0u8; BUF_SIZE]; let mut lines = 0u64; let mut check_last_byte_first_loop = true; - let mut bytes_remaining_to_search = file_size; + let mut bytes_remaining_to_search = file_size - start_offset; loop { // the casts here are ok, `buffer.len()` should never be above a few k let bytes_to_read_this_loop = bytes_remaining_to_search.min(buffer.len().try_into().unwrap()); - let read_start_offset = bytes_remaining_to_search - bytes_to_read_this_loop; + let read_start_offset = start_offset + bytes_remaining_to_search - bytes_to_read_this_loop; let buffer = &mut buffer[..bytes_to_read_this_loop.try_into().unwrap()]; bytes_remaining_to_search -= bytes_to_read_this_loop; @@ -359,8 +363,8 @@ where check_last_byte_first_loop = false; if buffer.last().is_some_and(|&b| b != separator) { if n == 0 { - input.rewind()?; - return Ok(file_size); + input.seek(SeekFrom::Start(start_offset))?; + return Ok(file_size - start_offset); } assert_eq!(lines, 0); lines = 1; @@ -370,14 +374,15 @@ where for separator_offset in memrchr_iter(separator, &buffer[..]) { lines += 1; if lines > n { - input.rewind()?; + input.seek(SeekFrom::Start(start_offset))?; return Ok(read_start_offset + TryInto::::try_into(separator_offset).unwrap() - + 1); + + 1 + - start_offset); } } - if read_start_offset == 0 { - input.rewind()?; + if read_start_offset == start_offset { + input.seek(SeekFrom::Start(start_offset))?; return Ok(0); } } @@ -412,11 +417,14 @@ fn head_backwards_without_seek_file(input: &mut File, options: &HeadOptions) -> fn head_backwards_on_seekable_file(input: &mut File, options: &HeadOptions) -> io::Result { match options.mode { Mode::AllButLastBytes(n) => { - let size = input.metadata()?.len(); - if n >= size { + let start_offset = input.stream_position()?; + let size = input.seek(SeekFrom::End(0))?; + let remaining = size - start_offset; + input.seek(SeekFrom::Start(start_offset))?; + if n >= remaining { Ok(0) } else { - print_n_bytes(input, size - n) + print_n_bytes(input, remaining - n) } } Mode::AllButLastLines(n) => { @@ -769,6 +777,19 @@ mod tests { ); } + #[test] + fn test_find_nth_line_from_end_respects_current_position() { + let mut input = Cursor::new(b"header\nx\ny\n".to_vec()); + input.seek(SeekFrom::Start(7)).unwrap(); + + assert_eq!(find_nth_line_from_end(&mut input, 0, b'\n').unwrap(), 4); + assert_eq!(input.stream_position().unwrap(), 7); + assert_eq!(find_nth_line_from_end(&mut input, 1, b'\n').unwrap(), 2); + assert_eq!(input.stream_position().unwrap(), 7); + assert_eq!(find_nth_line_from_end(&mut input, 2, b'\n').unwrap(), 0); + assert_eq!(input.stream_position().unwrap(), 7); + } + #[test] fn test_find_nth_line_from_end_non_terminated() { // Validate the find_nth_line_from_end for files that are not terminated with a final diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 4ab710793e0..3249093b723 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -678,6 +678,35 @@ fn test_validate_stdin_offset_lines() { .set_stdin(file_shadow) .succeeds() .stdout_only_fixture(seq_1001_20000_file_name); + + // Test 4 - All but the last n lines of a large file whose stdin offset is + // already past the first line. + scene + .cmd("seq") + .args(&["2", "1000"]) + .set_stdout(fixtures.make_file("seq_2_1000")) + .succeeds(); + scene + .cmd("seq") + .args(&["1001", "20000"]) + .set_stdout(fixtures.make_file("seq_1001_20000_offset")) + .succeeds(); + let file = fixtures.open(seq_20000_file_name); + let mut file_shadow = file.try_clone().unwrap(); + let mut first_line = [0u8; 2]; + file_shadow.read_exact(&mut first_line).unwrap(); + assert_eq!(&first_line, b"1\n"); + scene + .ucmd() + .args(&["-n", "-19000"]) + .set_stdin(file) + .succeeds() + .stdout_only_fixture("seq_2_1000"); + scene + .cmd("cat") + .set_stdin(file_shadow) + .succeeds() + .stdout_only_fixture("seq_1001_20000_offset"); } #[cfg(all( @@ -807,6 +836,36 @@ fn test_validate_stdin_offset_bytes() { .set_stdin(file_shadow) .succeeds() .stdout_only_fixture(seq_19001_20000_file_name); + + // Test 5 - All but the last n bytes of a large file whose stdin offset is + // already past the first line. + scene + .cmd("seq") + .args(&["2", "10000"]) + .set_stdout(fixtures.make_file("seq_2_10000")) + .succeeds(); + scene + .cmd("seq") + .args(&["10001", "20000"]) + .set_stdout(fixtures.make_file("seq_10001_20000")) + .succeeds(); + let file = fixtures.open(seq_20000_file_name); + let mut file_shadow = file.try_clone().unwrap(); + let mut first_line = [0u8; 2]; + file_shadow.read_exact(&mut first_line).unwrap(); + assert_eq!(&first_line, b"1\n"); + let suffix_length = fixtures.open("seq_10001_20000").metadata().unwrap().len(); + scene + .ucmd() + .args(&["-c", &format!("-{suffix_length}")]) + .set_stdin(file) + .succeeds() + .stdout_only_fixture("seq_2_10000"); + scene + .cmd("cat") + .set_stdin(file_shadow) + .succeeds() + .stdout_only_fixture("seq_10001_20000"); } #[cfg(all( From 6f471f7695261b6a09ef155bfd220494e25c3aa6 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Tue, 3 Nov 2026 08:17:26 +0800 Subject: [PATCH 2/2] head: avoid underflow when the stdin offset is past EOF --- src/uu/head/src/head.rs | 24 ++++++++++------------ tests/by-util/test_head.rs | 42 +++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index ad4bf1b53f2..7bdbb715b33 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -294,12 +294,12 @@ fn print_but_last_n_lines(mut input: impl Read, n: u64, separator: u8) -> io::Re Ok(bytes_written) } -/// Return the index in `input` just after the `n`th line from the end. +/// Return the byte offset, relative to the current position, just after the +/// `n`th line from the end of the remaining input. /// -/// If `n` exceeds the number of lines in this file, then return 0. -/// This function rewinds the cursor to the -/// beginning of the input just before returning unless there is an -/// I/O error. +/// If `n` exceeds the number of lines in the remaining input, then return 0. +/// This function restores the cursor to its original position just before +/// returning unless there is an I/O error. /// /// # Errors /// @@ -331,9 +331,7 @@ fn find_nth_line_from_end(input: &mut R, n: u64, separator: u8) -> io::Result where R: Read + Seek, { - // Only search the data after the current position, so a partly consumed - // seekable input (such as a regular file used as stdin) is handled - // relative to where the descriptor currently stands. + // Search only after the current position, so a partly consumed seekable input is handled relative to it. let start_offset = input.stream_position()?; let file_size = input.seek(SeekFrom::End(0))?; @@ -341,7 +339,7 @@ where let mut lines = 0u64; let mut check_last_byte_first_loop = true; - let mut bytes_remaining_to_search = file_size - start_offset; + let mut bytes_remaining_to_search = file_size.saturating_sub(start_offset); loop { // the casts here are ok, `buffer.len()` should never be above a few k @@ -417,10 +415,10 @@ fn head_backwards_without_seek_file(input: &mut File, options: &HeadOptions) -> fn head_backwards_on_seekable_file(input: &mut File, options: &HeadOptions) -> io::Result { match options.mode { Mode::AllButLastBytes(n) => { - let start_offset = input.stream_position()?; - let size = input.seek(SeekFrom::End(0))?; - let remaining = size - start_offset; - input.seek(SeekFrom::Start(start_offset))?; + let remaining = input + .metadata()? + .len() + .saturating_sub(input.stream_position()?); if n >= remaining { Ok(0) } else { diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 3249093b723..c6e6526649e 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -12,7 +12,7 @@ not(target_os = "openbsd"), not(windows) ))] -use std::io::Read; +use std::io::{Read, Seek, SeekFrom}; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; @@ -686,11 +686,6 @@ fn test_validate_stdin_offset_lines() { .args(&["2", "1000"]) .set_stdout(fixtures.make_file("seq_2_1000")) .succeeds(); - scene - .cmd("seq") - .args(&["1001", "20000"]) - .set_stdout(fixtures.make_file("seq_1001_20000_offset")) - .succeeds(); let file = fixtures.open(seq_20000_file_name); let mut file_shadow = file.try_clone().unwrap(); let mut first_line = [0u8; 2]; @@ -706,7 +701,40 @@ fn test_validate_stdin_offset_lines() { .cmd("cat") .set_stdin(file_shadow) .succeeds() - .stdout_only_fixture("seq_1001_20000_offset"); + .stdout_only_fixture(seq_1001_20000_file_name); +} + +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "android"), + not(target_os = "freebsd"), + not(target_os = "openbsd"), + not(windows) +))] +#[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: stdin file position not preserved through wasmtime" +)] +fn test_stdin_offset_past_eof() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + scene + .cmd("seq") + .arg("20000") + .set_stdout(fixtures.make_file("seq_20000")) + .succeeds(); + + for args in [["-n", "-1"], ["-c", "-1"]] { + let mut file = fixtures.open("seq_20000"); + file.seek(SeekFrom::End(100)).unwrap(); + scene + .ucmd() + .args(&args) + .set_stdin(file) + .succeeds() + .no_output(); + } } #[cfg(all(