From 7b1de7951e67cf028b685a687472281ef9b169f7 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Fri, 30 Oct 2026 05:05:41 +0800 Subject: [PATCH 1/5] od: print final offset when an input fails --- src/uu/od/src/od.rs | 6 +++--- tests/by-util/test_od.rs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 2b5d1c2b7f0..d719a1ad889 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -546,9 +546,9 @@ where let length = memory_decoder.length(); if length == 0 { - if !input_decoder.has_error() { - input_offset.write_final_offset(writer)?; - } + // GNU prints the final offset even when one of the inputs + // failed, so always emit it before stopping. + input_offset.write_final_offset(writer)?; break; } diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 5ebd456ce3d..1064e27f6e9 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -1748,3 +1748,15 @@ fn test_od_zero_byte_type_size() { .succeeds() .stdout_is("0000000 64636261\n0000004\n"); } + +#[test] +fn test_final_offset_printed_when_an_input_fails() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a.txt", "a\n"); + at.mkdir("d"); + + ucmd.args(&["a.txt", "d"]) + .fails_with_code(1) + .stdout_contains("0000002") + .stderr_contains("d: Is a directory"); +} From 6cdc067667809d0135282ed4f45f453051bc1be0 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Fri, 30 Oct 2026 19:28:24 +0800 Subject: [PATCH 2/5] od: keep the final offset for readable inputs, not unreadable ones --- src/uu/od/src/od.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index d719a1ad889..54e61fe8679 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -536,6 +536,7 @@ where { let mut duplicate_line = false; let mut previous_bytes: Vec = Vec::new(); + let mut input_was_read = false; let line_bytes = output_info.byte_size_line; loop { @@ -546,12 +547,17 @@ where let length = memory_decoder.length(); if length == 0 { - // GNU prints the final offset even when one of the inputs - // failed, so always emit it before stopping. - input_offset.write_final_offset(writer)?; + // GNU prints the final offset for an input it managed to read, + // even if a later input failed, but stays silent when no input + // could be opened at all (e.g. `od missing-file`). + if input_was_read || !input_decoder.has_error() { + input_offset.write_final_offset(writer)?; + } break; } + input_was_read = true; + // not enough byte for a whole element, this should only happen on the last line. if length != line_bytes { // set zero bytes in the part of the buffer that will be used, but is not filled. From 8a99f4692c94644dfa6995dc438c88b700f71aa9 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Sat, 31 Oct 2026 06:00:59 +0800 Subject: [PATCH 3/5] od: keep the failing-input test portable --- tests/by-util/test_od.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 1064e27f6e9..64e6f7f6226 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -1755,8 +1755,10 @@ fn test_final_offset_printed_when_an_input_fails() { at.write("a.txt", "a\n"); at.mkdir("d"); + // The message for an unreadable input differs between platforms (WASI says + // "Bad file descriptor"), so only the prefix is checked here. ucmd.args(&["a.txt", "d"]) .fails_with_code(1) .stdout_contains("0000002") - .stderr_contains("d: Is a directory"); + .stderr_contains("od: d:"); } From 9ca56d3b05829336f13e8e4b8fc9df198f219da6 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Tue, 3 Nov 2026 07:43:53 +0800 Subject: [PATCH 4/5] od: print the final offset for unreadable directories Track whether any input was opened so that a directory, which can be opened but not read, still gets its final offset printed like GNU. Tighten the failing-input test to the exact output and cover od d alone and od missing. --- src/uu/od/src/multifile_reader.rs | 10 ++++++++++ src/uu/od/src/od.rs | 24 ++++++++++++++---------- tests/by-util/test_od.rs | 27 ++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/uu/od/src/multifile_reader.rs b/src/uu/od/src/multifile_reader.rs index 782a4fcbc42..f7c4471e461 100644 --- a/src/uu/od/src/multifile_reader.rs +++ b/src/uu/od/src/multifile_reader.rs @@ -56,6 +56,7 @@ pub struct MultifileReader<'a> { ni: Vec>, curr_file: Option, any_err: bool, + any_opened: bool, file_name: Option<&'a OsStr>, } @@ -69,12 +70,19 @@ impl MultifileReader<'_> { ni: fnames, curr_file: None, // normally this means done; call next_file() any_err: false, + any_opened: false, file_name: None, }; mf.next_file(); mf } + /// Whether at least one input was opened successfully, even if reading it + /// failed afterwards. + pub fn has_input(&self) -> bool { + self.any_opened + } + fn next_file(&mut self) { // loop retries with subsequent files if err - normally 'loops' once loop { @@ -104,6 +112,7 @@ impl MultifileReader<'_> { { self.curr_file = Some(CurrentReader::Stdin(io::stdin())); } + self.any_opened = true; break; } InputSource::FileName(fname) => { @@ -113,6 +122,7 @@ impl MultifileReader<'_> { // No need to wrap `f` in a BufReader - buffered reading is taken care // of elsewhere. self.curr_file = Some(CurrentReader::File(f)); + self.any_opened = true; break; } Err(e) => { diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 54e61fe8679..23fe3809f75 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -283,7 +283,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut input_offset = InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label); - let mut input = open_input_peek_reader( + let (mut input, input_was_opened) = open_input_peek_reader( &od_options.input_strings, od_options.skip_bytes, od_options.read_bytes, @@ -305,6 +305,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &mut input_offset, &mut input_decoder, &output_info, + input_was_opened, &mut out, ) } @@ -528,6 +529,7 @@ fn odfunc( input_offset: &mut InputOffset, input_decoder: &mut InputDecoder, output_info: &OutputInfo, + input_was_opened: bool, writer: &mut W, ) -> UResult<()> where @@ -536,7 +538,6 @@ where { let mut duplicate_line = false; let mut previous_bytes: Vec = Vec::new(); - let mut input_was_read = false; let line_bytes = output_info.byte_size_line; loop { @@ -547,17 +548,16 @@ where let length = memory_decoder.length(); if length == 0 { - // GNU prints the final offset for an input it managed to read, - // even if a later input failed, but stays silent when no input - // could be opened at all (e.g. `od missing-file`). - if input_was_read || !input_decoder.has_error() { + // GNU prints the final offset for an input it managed to open, + // even if reading it failed (e.g. `od directory`), but stays + // silent when no input could be opened at all (e.g. + // `od missing-file`). + if input_was_opened || !input_decoder.has_error() { input_offset.write_final_offset(writer)?; } break; } - input_was_read = true; - // not enough byte for a whole element, this should only happen on the last line. if length != line_bytes { // set zero bytes in the part of the buffer that will be used, but is not filled. @@ -822,19 +822,23 @@ fn open_input_peek_reader( input_strings: &[OsString], skip_bytes: u64, read_bytes: Option, -) -> UResult>>>> { +) -> UResult<( + PeekReader>>>, + bool, +)> { // should return "impl PeekRead + Read + HasError" when supported in (stable) rust let inputs = map_input_strings(input_strings); let mut mf = MultifileReader::new(inputs); mf.skip(skip_bytes) .map_err(|e| USimpleError::new(1, e.to_string()))?; + let input_was_opened = mf.has_input(); let pr = PartialReader::new(mf, read_bytes); // Add a BufReader over the top of the PartialReader. This will have the // effect of generating buffered reads to files/stdin, but since these reads // go through MultifileReader (which limits the maximum number of bytes read) // we won't ever read more bytes than were specified with the `-N` flag. let buf_pr = BufReader::new(pr); - Ok(PeekReader::new(buf_pr)) + Ok((PeekReader::new(buf_pr), input_was_opened)) } impl HasError for BufReader { diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 64e6f7f6226..8d4014fe413 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -1759,6 +1759,31 @@ fn test_final_offset_printed_when_an_input_fails() { // "Bad file descriptor"), so only the prefix is checked here. ucmd.args(&["a.txt", "d"]) .fails_with_code(1) - .stdout_contains("0000002") + .stdout_is("0000000 005141\n0000002\n") .stderr_contains("od: d:"); } + +#[test] +#[cfg(not(windows))] +#[cfg_attr(wasi_runner, ignore)] +fn test_final_offset_printed_for_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("d"); + + // A directory can be opened but not read, so GNU still prints the final + // offset even though no data was decoded. + ucmd.arg("d") + .fails_with_code(1) + .stdout_is("0000000\n") + .stderr_contains("od: d:"); +} + +#[test] +fn test_no_final_offset_when_no_input_can_be_opened() { + let (_at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg("missing") + .fails_with_code(1) + .no_stdout() + .stderr_contains("od: missing:"); +} From 48593893199abfbe9d8e8fa5aad8a8670b2d94f4 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Wed, 4 Nov 2026 06:30:27 +0800 Subject: [PATCH 5/5] od: only print final offset after input is consumed --- src/uu/od/src/input_offset.rs | 5 +++++ src/uu/od/src/multifile_reader.rs | 10 ---------- src/uu/od/src/od.rs | 21 +++++++-------------- tests/by-util/test_od.rs | 22 ++++++++++++++++++---- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/uu/od/src/input_offset.rs b/src/uu/od/src/input_offset.rs index 6e5e36b5579..bb91b5439c5 100644 --- a/src/uu/od/src/input_offset.rs +++ b/src/uu/od/src/input_offset.rs @@ -35,6 +35,11 @@ impl InputOffset { } } + /// Whether no input has been consumed yet, including skipped bytes. + pub fn is_at_start(&self) -> bool { + self.byte_pos == 0 + } + /// Increase `byte_pos` and `label` if a label is used. pub fn increase_position(&mut self, n: u64) { self.byte_pos += n; diff --git a/src/uu/od/src/multifile_reader.rs b/src/uu/od/src/multifile_reader.rs index f7c4471e461..782a4fcbc42 100644 --- a/src/uu/od/src/multifile_reader.rs +++ b/src/uu/od/src/multifile_reader.rs @@ -56,7 +56,6 @@ pub struct MultifileReader<'a> { ni: Vec>, curr_file: Option, any_err: bool, - any_opened: bool, file_name: Option<&'a OsStr>, } @@ -70,19 +69,12 @@ impl MultifileReader<'_> { ni: fnames, curr_file: None, // normally this means done; call next_file() any_err: false, - any_opened: false, file_name: None, }; mf.next_file(); mf } - /// Whether at least one input was opened successfully, even if reading it - /// failed afterwards. - pub fn has_input(&self) -> bool { - self.any_opened - } - fn next_file(&mut self) { // loop retries with subsequent files if err - normally 'loops' once loop { @@ -112,7 +104,6 @@ impl MultifileReader<'_> { { self.curr_file = Some(CurrentReader::Stdin(io::stdin())); } - self.any_opened = true; break; } InputSource::FileName(fname) => { @@ -122,7 +113,6 @@ impl MultifileReader<'_> { // No need to wrap `f` in a BufReader - buffered reading is taken care // of elsewhere. self.curr_file = Some(CurrentReader::File(f)); - self.any_opened = true; break; } Err(e) => { diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 23fe3809f75..94c8fb1eca5 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -283,7 +283,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut input_offset = InputOffset::new(od_options.radix, od_options.skip_bytes, od_options.label); - let (mut input, input_was_opened) = open_input_peek_reader( + let mut input = open_input_peek_reader( &od_options.input_strings, od_options.skip_bytes, od_options.read_bytes, @@ -305,7 +305,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &mut input_offset, &mut input_decoder, &output_info, - input_was_opened, &mut out, ) } @@ -529,7 +528,6 @@ fn odfunc( input_offset: &mut InputOffset, input_decoder: &mut InputDecoder, output_info: &OutputInfo, - input_was_opened: bool, writer: &mut W, ) -> UResult<()> where @@ -548,11 +546,10 @@ where let length = memory_decoder.length(); if length == 0 { - // GNU prints the final offset for an input it managed to open, - // even if reading it failed (e.g. `od directory`), but stays - // silent when no input could be opened at all (e.g. - // `od missing-file`). - if input_was_opened || !input_decoder.has_error() { + // After a read error, print the final offset only when + // some input was already consumed; otherwise stay silent + // (e.g. `od directory` or `od missing`). + if !input_decoder.has_error() || !input_offset.is_at_start() { input_offset.write_final_offset(writer)?; } break; @@ -822,23 +819,19 @@ fn open_input_peek_reader( input_strings: &[OsString], skip_bytes: u64, read_bytes: Option, -) -> UResult<( - PeekReader>>>, - bool, -)> { +) -> UResult>>>> { // should return "impl PeekRead + Read + HasError" when supported in (stable) rust let inputs = map_input_strings(input_strings); let mut mf = MultifileReader::new(inputs); mf.skip(skip_bytes) .map_err(|e| USimpleError::new(1, e.to_string()))?; - let input_was_opened = mf.has_input(); let pr = PartialReader::new(mf, read_bytes); // Add a BufReader over the top of the PartialReader. This will have the // effect of generating buffered reads to files/stdin, but since these reads // go through MultifileReader (which limits the maximum number of bytes read) // we won't ever read more bytes than were specified with the `-N` flag. let buf_pr = BufReader::new(pr); - Ok((PeekReader::new(buf_pr), input_was_opened)) + Ok(PeekReader::new(buf_pr)) } impl HasError for BufReader { diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 8d4014fe413..e4182e0ca52 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -1766,15 +1766,29 @@ fn test_final_offset_printed_when_an_input_fails() { #[test] #[cfg(not(windows))] #[cfg_attr(wasi_runner, ignore)] -fn test_final_offset_printed_for_directory() { +fn test_no_final_offset_for_directory() { let (at, mut ucmd) = at_and_ucmd!(); at.mkdir("d"); - // A directory can be opened but not read, so GNU still prints the final - // offset even though no data was decoded. + // A directory can be opened but not read; with no input consumed, there is + // no final offset to report. ucmd.arg("d") .fails_with_code(1) - .stdout_is("0000000\n") + .no_stdout() + .stderr_contains("od: d:"); +} + +#[test] +#[cfg(not(windows))] +#[cfg_attr(wasi_runner, ignore)] +fn test_no_final_offset_for_directory_with_limited_read() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("d"); + + // Same as above, with -N limiting the read to one byte. + ucmd.args(&["-N1", "d"]) + .fails_with_code(1) + .no_stdout() .stderr_contains("od: d:"); }