diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index f07c2b393..ecd08264f 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -194,42 +194,59 @@ fn fold_file_bytewise( let mut line = Vec::new(); loop { - // Ensures that our line always has enough bytes to either - // come across a newline, a whitespace or just wrap around. - while width > line.len() { + // Pull bytes from the reader until we have strictly more than `width` + // buffered (enough to know whether content follows a width-driven fold) + // or we reach EOF. Reading at most `width` bytes ahead keeps memory + // bounded even on endless streams like /dev/zero, where the old + // read_until(NL, ..) would buffer forever waiting for a newline. + while line.len() <= width { let buf = file .fill_buf() .map_err_context(|| translate!("fold-error-readline"))?; if buf.is_empty() { break; } - line.extend_from_slice(buf); let len = buf.len(); + line.extend_from_slice(buf); file.consume(len); } - // The width exceeds the line read so far if we have - // reached EOF. - if width > line.len() { + // EOF with a tail shorter than (or equal to) `width`: no width/space + // fold can apply, so emit it verbatim (newlines inside are preserved). + if line.len() <= width { + if line.is_empty() { + break; + } output.write_all(&line)?; break; } + // We have a full `width`-byte chunk plus at least one lookahead byte. let chunk = &line[..width]; - let newline_end = chunk.iter().position(|c| NL.eq(c)).map(|v| v + 1); - let space_end = chunk - .iter() - .rposition(|c| spaces && c.is_ascii_whitespace() && !CR.eq(c)) - .map(|v| v + 1); + // An existing newline within the chunk ends the line naturally; the + // newline is part of the slice, so no extra newline is emitted. + if let Some(end) = chunk.iter().position(|c| *c == NL).map(|i| i + 1) { + output.write_all(&line[..end])?; + line.drain(..end); + continue; + } - let end = newline_end.or(space_end).unwrap_or(width); - let slice = &line[..end]; + // No newline: with -s, break after the last whitespace (excluding CR); + // otherwise hard-wrap at `width`. + let end = if spaces { + chunk + .iter() + .rposition(|c| c.is_ascii_whitespace() && *c != CR) + .map_or(width, |i| i + 1) + } else { + width + }; - output.write_all(slice)?; - let slice_ends_without_newline = line[end - 1] != NL; - let no_newline_follows_slice = line.get(end).is_some_and(|c| *c != NL); - if slice_ends_without_newline && no_newline_follows_slice { + output.write_all(&line[..end])?; + // Width/space-driven fold: insert a newline unless the next byte is + // already a newline (it is emitted on the next pass). + if line[end] != NL { output.write_all(&[NL])?; } line.drain(..end); diff --git a/tests/by-util/test_fold.rs b/tests/by-util/test_fold.rs index 2d863ffa4..23ac6c989 100644 --- a/tests/by-util/test_fold.rs +++ b/tests/by-util/test_fold.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fullwidth refgh tefgh nefgh +// spell-checker:ignore fullwidth refgh tefgh nefgh unflushed use bytecount::count; use unicode_width::UnicodeWidthChar; @@ -891,6 +891,10 @@ fn test_bytewise_carriage_return_is_not_word_boundary() { } #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: killing the wasmtime process discards the unflushed output buffer, so the streamed bytes never reach stdout" +)] #[test] fn test_bytewise_read_from_pseudo_device() { let mut child = new_ucmd!().arg("-b").arg("/dev/zero").run_no_wait(); @@ -905,6 +909,27 @@ fn test_bytewise_read_from_pseudo_device() { .no_stderr(); } +/// A fold boundary that lands exactly on the read-buffer boundary must still +/// insert the fold newline. The streaming reader fills at most `BufReader` +/// capacity per call, so folding at `width == capacity` is the case where the +/// byte following the fold point is not yet buffered. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn test_bytewise_fold_at_read_buffer_boundary() { + let width = buf_reader_capacity(); + let input = vec![b'a'; width * 2]; + + let mut expected = vec![b'a'; width]; + expected.push(b'\n'); + expected.extend(std::iter::repeat_n(b'a', width)); + + new_ucmd!() + .args(&["-b", &format!("-w{width}")]) + .pipe_in(input) + .succeeds() + .stdout_is_bytes(expected); +} + #[test] fn test_obsolete_syntax() { new_ucmd!()