This change is performance neutral as tested with:
 $ yes | head -n10M > sl.in
 $ yes $(yes éééááé | head -n9 | paste -s -d,) | head -n1M > mb.in

 $ hyperfine src/cut{,.orig}' -c1 sl.in >/dev/null'
 $ hyperfine src/cut{,.orig}' -f1 -w mb.in >/dev/null'

* src/cut.c (skip_buffered_line): A new refactored helper.
* src/cut.c (scan_mb_line_end): Use help, and also call mbbuf_avail()
to process remaining bytes before topup.
(cut_characters_mode): Likewise. Note the topup is done implicitly
in the following mbbuf_get_char().
* NEWS: Mention the bug fix.

Discovered by Anthropic's OSS Scanner, as bug ANT-2026-VRMLC6DR.
---
 NEWS      |  5 +++++
 src/cut.c | 30 ++++++++++++++++++++----------
 2 files changed, 25 insertions(+), 10 deletions(-)

diff --git a/NEWS b/NEWS
index ffe134e76..c64f7b481 100644
--- a/NEWS
+++ b/NEWS
@@ -49,6 +49,11 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   the \u0085 (next line) character as a separator on non-GLIBC platforms.
   [bug introduced in coreutils-9.11]
 
+  'cut -w', and 'cut -d' in some multi-byte locales, no longer delay output
+  of an already read line until more input arrives, when fewer than 4 bytes
+  follow the last selected field.
+  [bug introduced in coreutils-9.12]
+
   'chown -v' and 'chgrp -v' no longer emit uninitialized user and group IDs 
when
   operating on dangling symbolic links.
   [bug introduced in coreutils-5.3.0]
diff --git a/src/cut.c b/src/cut.c
index 8e7ed3fac..b438f661f 100644
--- a/src/cut.c
+++ b/src/cut.c
@@ -442,6 +442,18 @@ search_bytes (char const *buf, int c, size_t n_bytes)
   return 2 < n_bytes ? (char *) memchr (buf + 2, c, n_bytes - 2) : NULL;
 }
 
+/* Consume buffered bytes through the next line delimiter, if present.
+   Return whether a delimiter was consumed.  Do not refill the buffer.  */
+
+static inline bool
+skip_buffered_line (mbbuf_t *mbbuf, idx_t available)
+{
+  char *p = mbbuf->buffer + mbbuf->offset;
+  char *end = search_bytes (p, line_delim, available);
+  mbbuf_advance (mbbuf, end ? end - p + 1 : available);
+  return end != NULL;
+}
+
 static inline void
 write_line_delim (void)
 {
@@ -591,14 +603,13 @@ scan_mb_line_end (mbbuf_t *mbbuf, struct mbfield_parser 
*parser,
 
   while (true)
     {
-      idx_t available = mbbuf_topup (mbbuf);
+      idx_t available = mbbuf_avail (mbbuf);
+      if (available == 0)
+        available = mbbuf_topup (mbbuf);
       if (available == 0)
         return FIELD_EOF;
 
-      char *buffer = mbbuf->buffer + mbbuf->offset;
-      char *line_end = search_bytes (buffer, line_delim, available);
-      idx_t n = line_end ? line_end - buffer + 1 : available;
-      mbbuf_advance (mbbuf, n);
+      bool line_end = skip_buffered_line (mbbuf, available);
       *have_pending_line = true;
 
       if (line_end)
@@ -929,11 +940,10 @@ cut_characters_mode (FILE *stream, bool byte_mode)
       /* Skip unselected line suffixes without decoding each character.  */
       if (idx && field_selection_exhausted (idx))
         {
-          idx_t available = mbbuf_topup (&mbbuf);
-          char *p = mbbuf.buffer + mbbuf.offset;
-          char *end = search_bytes (p, line_delim, available);
-          mbbuf_advance (&mbbuf, end ? end - p : available);
-          if (!end && available)
+          idx_t available = mbbuf_avail (&mbbuf);
+          if (skip_buffered_line (&mbbuf, available))
+            reset_item_line (&idx, &print_delimiter);
+          if (available)
             continue;
         }
 
-- 
2.56.0


Reply via email to