Skip to content

Commit 0e675e9

Browse files
committed
od: Ensure stdin is left in the correct state
Fixes issue #7666 For `od` utility, if client specifies `-N` maximum bytes to be read then ensure stdin is left pointing to the next byte when `od` exits. To do this... - Bypass standard buffering on stdin. - Instantiate BufReader further up the stack to maintain performance.
1 parent 349e568 commit 0e675e9

File tree

3 files changed

+74
-10
lines changed

3 files changed

+74
-10
lines changed

src/uu/od/src/multifilereader.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
// spell-checker:ignore (ToDO) multifile curr fnames fname xfrd fillloop mockstream
66

77
use std::fs::File;
8-
use std::io::{self, BufReader};
8+
use std::io;
9+
#[cfg(unix)]
10+
use std::os::fd::{AsRawFd, FromRawFd};
911

1012
use uucore::display::Quotable;
1113
use uucore::show_error;
@@ -48,13 +50,36 @@ impl MultifileReader<'_> {
4850
}
4951
match self.ni.remove(0) {
5052
InputSource::Stdin => {
51-
self.curr_file = Some(Box::new(BufReader::new(io::stdin())));
53+
// In order to pass GNU compatibility tests, when the client passes in the
54+
// `-N` flag we must not read any bytes beyond that limit. As such, we need
55+
// to disable the default buffering for stdin below.
56+
// For performance reasons we do still do buffered reads from stdin, but
57+
// the buffering is done elsewhere and in a way that is aware of the `-N`
58+
// limit.
59+
let stdin = io::stdin();
60+
#[cfg(unix)]
61+
{
62+
let stdin_raw_fd = stdin.as_raw_fd();
63+
let stdin_file = unsafe { File::from_raw_fd(stdin_raw_fd) };
64+
self.curr_file = Some(Box::new(stdin_file));
65+
}
66+
67+
// For non-unix platforms we don't have GNU compatibility requirements, so
68+
// we don't need to prevent stdin buffering. This is sub-optimal (since
69+
// there will still be additional buffering further up the stack), but
70+
// doesn't seem worth worrying about at this time.
71+
#[cfg(not(unix))]
72+
{
73+
self.curr_file = Some(Box::new(stdin));
74+
}
5275
break;
5376
}
5477
InputSource::FileName(fname) => {
5578
match File::open(fname) {
5679
Ok(f) => {
57-
self.curr_file = Some(Box::new(BufReader::new(f)));
80+
// No need to wrap `f` in a BufReader - buffered reading is taken care
81+
// of elsewhere.
82+
self.curr_file = Some(Box::new(f));
5883
break;
5984
}
6085
Err(e) => {

src/uu/od/src/od.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod prn_int;
2626

2727
use std::cmp;
2828
use std::fmt::Write;
29+
use std::io::{BufReader, Read};
2930

3031
use crate::byteorder_io::ByteOrder;
3132
use crate::formatteriteminfo::FormatWriter;
@@ -603,7 +604,7 @@ fn open_input_peek_reader(
603604
input_strings: &[String],
604605
skip_bytes: u64,
605606
read_bytes: Option<u64>,
606-
) -> PeekReader<PartialReader<MultifileReader>> {
607+
) -> PeekReader<BufReader<PartialReader<MultifileReader>>> {
607608
// should return "impl PeekRead + Read + HasError" when supported in (stable) rust
608609
let inputs = input_strings
609610
.iter()
@@ -615,7 +616,18 @@ fn open_input_peek_reader(
615616

616617
let mf = MultifileReader::new(inputs);
617618
let pr = PartialReader::new(mf, skip_bytes, read_bytes);
618-
PeekReader::new(pr)
619+
// Add a BufReader over the top of the PartialReader. This will have the
620+
// effect of generating buffered reads to files/stdin, but since these reads
621+
// go through MultifileReader (which limits the maximum number of bytes read)
622+
// we won't ever read more bytes than were specified with the `-N` flag.
623+
let buf_pr = BufReader::new(pr);
624+
PeekReader::new(buf_pr)
625+
}
626+
627+
impl<R: Read + HasError> HasError for BufReader<R> {
628+
fn has_error(&self) -> bool {
629+
self.get_ref().has_error()
630+
}
619631
}
620632

621633
fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String {

tests/by-util/test_od.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
// spell-checker:ignore abcdefghijklmnopqrstuvwxyz Anone
77

8+
use std::io::Read;
9+
810
use unindent::unindent;
911
use uutests::at_and_ucmd;
1012
use uutests::new_ucmd;
@@ -660,14 +662,39 @@ fn test_skip_bytes_error() {
660662

661663
#[test]
662664
fn test_read_bytes() {
665+
let scene = TestScenario::new(util_name!());
666+
let fixtures = &scene.fixtures;
667+
663668
let input = "abcdefghijklmnopqrstuvwxyz\n12345678";
664-
new_ucmd!()
669+
670+
fixtures.write("f1", input);
671+
let file = fixtures.open("f1");
672+
let mut file_shadow = file.try_clone().unwrap();
673+
674+
scene
675+
.ucmd()
665676
.arg("--endian=little")
666677
.arg("--read-bytes=27")
667-
.run_piped_stdin(input.as_bytes())
668-
.no_stderr()
669-
.success()
670-
.stdout_is(unindent(ALPHA_OUT));
678+
.set_stdin(file)
679+
.succeeds()
680+
.stdout_only(unindent(ALPHA_OUT));
681+
682+
// On unix platforms, confirm that only 27 bytes and strictly no more were read from stdin.
683+
// Leaving stdin in the correct state is required for GNU compatibilty.
684+
#[cfg(unix)]
685+
{
686+
// skip(27) to skip the 27 bytes that should have been consumed with the
687+
// --read-bytes flag.
688+
let expected_bytes_remaining_in_stdin: Vec<_> = input.bytes().skip(27).collect();
689+
let mut bytes_remaining_in_stdin = vec![];
690+
assert_eq!(
691+
file_shadow
692+
.read_to_end(&mut bytes_remaining_in_stdin)
693+
.unwrap(),
694+
expected_bytes_remaining_in_stdin.len()
695+
);
696+
assert_eq!(expected_bytes_remaining_in_stdin, bytes_remaining_in_stdin);
697+
}
671698
}
672699

673700
#[test]

0 commit comments

Comments
 (0)