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
65 changes: 25 additions & 40 deletions src/uu/wc/src/count_fast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::WordCountable;
use std::io::{self, ErrorKind, Read};

#[cfg(unix)]
use std::io::{Seek, SeekFrom};
use rustix::fs::SeekFrom;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
#[cfg(windows)]
Expand Down Expand Up @@ -85,55 +85,40 @@ pub(crate) fn count_bytes_fast<T: WordCountable>(handle: &mut T) -> (usize, Opti
// In such cases - attempt `seek()` almost to the end of the file
// and then fall back on read to count the rest.
//
// And finally a special case of input redirection in *nix shell:
// `( wc -c ; wc -c ) < file` should return
// ```
// size_of_file
// 0
// ```
// Similarly
// `( head -c1 ; wc -c ) < file` should return
// ```
// first_byte_of_file
// size_of_file - 1
// ```
// Since the input stream from file is treated as continuous across both commands inside ().
// In cases like this, due to `<` redirect, the `stat.st_mode` would report input as a regular file
// and `stat.st_size` would report the size of file on disk
// and NOT the remaining number of bytes in the input stream.
// However, the raw file descriptor in this situation would be equal to `0`
// for STDIN in both invocations.
// Therefore we cannot rely of `st_size` here and should fall back on full read.
if handle.as_raw_fd() > 0
&& (stat.st_mode as libc::mode_t & libc::S_IFREG) != 0
// The descriptor may already be partway through a redirected file.
// Count only the unread suffix and advance it so a later `-` operand or
// another process sharing stdin sees EOF.
if (stat.st_mode as libc::mode_t & libc::S_IFREG) != 0
&& stat.st_size > 0
&& let Ok(position) = rustix::fs::seek(&handle, SeekFrom::Current(0))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this now changes the result of ( head -c1; wc -c ) < file, not only the speed.
could you please keep one test for the partial offset case? the old code had a full comment just to protect it

{
let sys_page_size = rustix::param::page_size();
if !(stat.st_size as usize).is_multiple_of(sys_page_size) {
// regular file or file from /proc, /sys and similar pseudo-filesystems
// with size that is NOT a multiple of system page size
return (stat.st_size as usize, None);
} else if let Some(file) = handle.inner_file() {
// On some platforms `stat.st_blksize` and `stat.st_size`
// are of different types: i64 vs i32
// i.e. MacOS on Apple Silicon (aarch64-apple-darwin),
// Debian Linux on ARM (aarch64-unknown-linux-gnu),
// 32bit i686 targets, etc.
// While on the others they are of the same type.
let size = stat.st_size as u64;
if position >= size {
return (0, None);
}
let known_size = !(size as usize).is_multiple_of(rustix::param::page_size());
let offset = if known_size {
size
} else {
// Probe the final block of page-aligned files: pseudo-files can
// advertise a full page even when their contents are shorter.
// st_blksize has different integer types across supported targets.
#[allow(clippy::unnecessary_cast)]
let offset =
stat.st_size as i64 - stat.st_size as i64 % (stat.st_blksize as i64 + 1);

if let Ok(n) = file.seek(SeekFrom::Start(offset as u64)) {
byte_count = n as usize;
let block_size = stat.st_blksize as u64;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment explaining why this allow is needed was removed, please keep a short version

(size - size % (block_size + 1)).max(position)
};
if let Ok(offset) = rustix::fs::seek(&handle, SeekFrom::Start(offset)) {
byte_count = (offset - position) as usize;
if known_size {
return (byte_count, None);
}
}
}
// Else, if we're on Linux, we use splice with broker (mostly for stream)
#[cfg(any(target_os = "linux", target_os = "android"))]
match count_bytes_using_splice(handle) {
Ok(n) => return (byte_count + n, None),
Err(n) => byte_count = n,
Err(n) => byte_count += n,
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/uu/wc/src/countable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ use std::fs::File;
use std::io::{BufRead, BufReader, Read, StdinLock};

#[cfg(unix)]
use std::os::fd::{AsFd, AsRawFd};
use std::os::fd::AsFd;

#[cfg(unix)]
pub trait WordCountable: AsFd + AsRawFd + Read {
pub trait WordCountable: AsFd + Read {
type Buffered: BufRead;
fn buffered(self) -> Self::Buffered;
fn inner_file(&mut self) -> Option<&mut File>;
}

#[cfg(all(not(unix), not(target_os = "wasi")))]
Expand All @@ -42,6 +41,7 @@ impl WordCountable for StdinLock<'_> {
fn buffered(self) -> Self::Buffered {
self
}
#[cfg(not(unix))]
fn inner_file(&mut self) -> Option<&mut File> {
None
}
Expand All @@ -64,6 +64,7 @@ impl WordCountable for File {
BufReader::new(self)
}

#[cfg(not(unix))]
fn inner_file(&mut self) -> Option<&mut File> {
Some(self)
}
Expand Down
36 changes: 36 additions & 0 deletions tests/by-util/test_wc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,3 +760,39 @@ fn test_posixly_correct_whitespace() {
.succeeds()
.stdout_is("1\n");
}

#[cfg(unix)]
#[rstest::rstest]
#[case::partial(6, 1)]
#[case::at_eof(6, 6)]
#[case::page_aligned(4096, 0)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please add a page_aligned case with a non zero offset too? nothing covers the .max(position) part

#[case::page_aligned_partial(4096, 4095)]
fn test_count_bytes_from_partial_stdin_offset(#[case] size: u64, #[case] offset: u64) {
use std::io::{Seek, SeekFrom};
let (at, mut command) = at_and_ucmd!();
at.write("input", &"x".repeat(usize::try_from(size).unwrap()));
let mut input = std::fs::File::open(at.plus("input")).unwrap();
input.seek(SeekFrom::Start(offset)).unwrap();
let shared = input.try_clone().unwrap();
command
.arg("-c")
.set_stdin(shared)
.succeeds()
.stdout_only(format!("{}\n", size - offset));
assert_eq!(input.stream_position().unwrap(), size);
}

#[cfg(unix)]
#[test]
fn test_count_bytes_from_repeated_redirected_stdin() {
use std::io::Seek;
let (at, mut command) = at_and_ucmd!();
at.write("input", "abcdef");
let mut input = std::fs::File::open(at.plus("input")).unwrap();
command
.args(&["-c", "-", "-"])
.set_stdin(input.try_clone().unwrap())
.succeeds()
.stdout_only(" 6 -\n 0 -\n 6 total\n");
assert_eq!(input.stream_position().unwrap(), 6);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please also add a test for wc -c - - with a file as stdin? it should print the size then 0
the description still mentions it, along with the EOF and page-aligned tests

}
Loading