ncurses 6.6.20251231, wide-character build (libncursesw), Linux/glibc.
(The code path is present in every wide build I checked, ncurses 5.9 through 6.6.)

On a wide-character build, inserting a printable 8-bit byte > 127 with
winsch()/insch() stores the byte as a code point directly, without converting it through the locale, whereas waddch() with the same byte runs the normal mbtowc conversion.  So under a non-Latin-1 8-bit locale addch() decodes the byte and
insch() does not.

Under en_US.ISO-8859-15, byte 0xA4 is EURO SIGN (U+20AC): addch(0xA4) stores the
euro, but insch(0xA4) stores U+00A4 instead -- a different character.  For a
Latin-1 locale the bug is invisible, since there the byte equals the code point
(0xA4 -> U+00A4 either way), which is presumably why it has gone unnoticed.

  #include <locale.h>
  #include <curses.h>
  #include <stdio.h>

  int main(void) {
      char a[4] = {0}, b[4] = {0};
      setlocale(LC_ALL, "");
      initscr();
      addch(0xA4);                 /* addch decodes the byte via the locale */
      mvinsch(1, 0, 0xA4);         /* insch under test */
      mvwinnstr(stdscr, 0, 0, a, 1);
      mvwinnstr(stdscr, 1, 0, b, 1);
      endwin();
      fprintf(stderr, "addch(0xA4): winnstr=0x%02X\n", (unsigned char)a[0]);       fprintf(stderr, "insch(0xA4): winnstr=0x%02X\n", (unsigned char)b[0]);
      return 0;
  }

  $ cc repro.c -o repro -lncursesw
  $ TERM=xterm LC_ALL=en_US.ISO-8859-15 ./repro

winnstr() reads each cell back as a locale byte.  Expected, with insch matching
addch, is 0xA4 for both.  Actual:

  addch(0xA4): winnstr=0xA4         (the euro, read back correctly)
  insch(0xA4): winnstr=0x20         (a blank: U+00A4 is not in ISO-8859-15, so
                                     the inserted euro was lost)

(Use addch(0xA4), not addstr("euro"): a UTF-8 source literal is misread under an
8-bit locale.)

In ncurses/base/lib_insch.c, _nc_insert_ch() handles a printable 8-bit byte with
a fast path that builds the cell straight from the byte:

      if (... (isprint(ch8) || ...)) {
          ...
          SetChar2(wch, ch);          /* byte used directly as the code point */
      }

The locale conversion, _nc_build_wch() (mbtowc), is only reached in the trailing
"else" branch, for a byte that is not isprint() in the current locale.
waddch()/wadd_wch() route every non-ASCII byte through _nc_build_wch(), so
addch() decodes 0xA4 to U+20AC while insch() short-circuits to U+00A4.  A fix would let the wide build take the _nc_build_wch() path for an 8-bit byte in the
insert case too (the isprint() fast path is fine for ASCII).

This is the write-side companion to the winch()/inch() locale issue reported
separately: there winnstr() decodes a cell via the locale while winch() returns the truncated code point.  Both come from the legacy 8-bit chtype API treating a
byte inconsistently across functions on a wide build.


Reply via email to