mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
fold: insert fold newline at read-buffer boundary (#12682)
follow up of #11314
This commit is contained in:
+35
-18
@@ -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);
|
||||
|
||||
@@ -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!()
|
||||
|
||||
Reference in New Issue
Block a user