fold: insert fold newline at read-buffer boundary (#12682)

follow up of #11314
This commit is contained in:
Sylvestre Ledru
2026-06-07 21:41:59 +02:00
committed by GitHub
parent 6bd3a39c3a
commit b2ddbabed3
2 changed files with 61 additions and 19 deletions
+35 -18
View File
@@ -194,42 +194,59 @@ fn fold_file_bytewise<T: Read, W: Write>(
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);
+26 -1
View File
@@ -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!()