Repository navigation
wc: seek through redirected regular input for byte counts #15492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)] | ||
|
|
@@ -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)) | ||
| { | ||
| 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| #[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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you please also add a test for |
||
| } | ||
There was a problem hiding this comment.
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