Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 35 additions & 16 deletions src/uu/head/src/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -331,19 +331,21 @@ fn find_nth_line_from_end<R>(input: &mut R, n: u64, separator: u8) -> io::Result
where
R: Read + Seek,
{
// Search only after the current position, so a partly consumed seekable input is handled relative to it.
let start_offset = input.stream_position()?;
Comment thread
Logic769 marked this conversation as resolved.
Resolved
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.saturating_sub(start_offset);

Comment thread
Logic769 marked this conversation as resolved.
Resolved
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;

Expand All @@ -359,8 +361,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;
Expand All @@ -370,14 +372,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::<u64>::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);
}
}
Expand Down Expand Up @@ -412,11 +415,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<u64> {
match options.mode {
Mode::AllButLastBytes(n) => {
let size = input.metadata()?.len();
if n >= size {
let remaining = input
.metadata()?
.len()
.saturating_sub(input.stream_position()?);
if n >= remaining {
Ok(0)
} else {
print_n_bytes(input, size - n)
print_n_bytes(input, remaining - n)
}
}
Mode::AllButLastLines(n) => {
Expand Down Expand Up @@ -769,6 +775,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
Expand Down
89 changes: 88 additions & 1 deletion tests/by-util/test_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -678,6 +678,63 @@ 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();
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_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(
Expand Down Expand Up @@ -807,6 +864,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(
Expand Down
Loading