From 0777f505352b78c4d98cc20a78bd84e8406d6312 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 29 Jul 2026 09:33:37 +0300 Subject: [PATCH 1/2] gh-154855: Ask non-ncurses curses for one more character Passing n to the library is ncurses' reading of n: it stores n characters and adds a terminator. NetBSD curses counts the terminator in n. Ask a library that is neither ncurses nor PDCurses for n + 1, and read again if it stored more than asked; truncating could split a multibyte character. This is not possible for input, so getstr() and get_wstr() are left as they are. instr() now takes the length from the value returned by winnstr(), as X/Open specifies, instead of searching for a terminator which it does not. --- Modules/_cursesmodule.c | 55 ++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index b8680edc6c0bed..2a82c60918b37f 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -309,6 +309,14 @@ curses_window_set_null_error(PyCursesWindowObject *win, _curses_set_null_error(state, curses_funcname, python_funcname); } +/* ncurses and PDCurses store n characters and add a terminator; NetBSD + curses counts the terminator in n. Ask an unknown library for one more. */ +#if defined(NCURSES_VERSION) || defined(PDCURSES) +# define CURSES_STR_EXTRA 0 +#else +# define CURSES_STR_EXTRA 1 +#endif + /* Utility Checking Procedures */ /* @@ -3796,25 +3804,33 @@ curses_window_instr_bytes(PyCursesWindowObject *self, PyObject *args, return NULL; } - n = Py_MIN(n, max_buf_size - 1); + n = Py_MIN(n, max_buf_size - 1 - CURSES_STR_EXTRA); + n += CURSES_STR_EXTRA; PyBytesWriter *writer = PyBytesWriter_Create(n + 1); if (writer == NULL) { return NULL; } char *buf = PyBytesWriter_GetData(writer); - if (use_xy) { - rtn = mvwinnstr(self->win, y, x, buf, n); - } - else { - rtn = winnstr(self->win, buf, n); + /* Read again if the library stored more than asked: truncating could + split a multibyte character. */ + for (unsigned int want = n - CURSES_STR_EXTRA; ; n = want) { + if (use_xy) { + rtn = mvwinnstr(self->win, y, x, buf, n); + } + else { + rtn = winnstr(self->win, buf, n); + } + if (rtn == ERR || (unsigned int)rtn <= want) { + break; + } } if (rtn == ERR) { PyBytesWriter_Discard(writer); return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); } - return PyBytesWriter_FinishWithSize(writer, strlen(buf)); + return PyBytesWriter_FinishWithSize(writer, rtn); } static PyObject * @@ -3939,17 +3955,25 @@ PyCursesWindow_in_wstr(PyObject *op, PyObject *args) return NULL; } - n = Py_MIN(n, max_buf_size - 1); + n = Py_MIN(n, max_buf_size - 1 - CURSES_STR_EXTRA); + n += CURSES_STR_EXTRA; wchar_t *buf = PyMem_New(wchar_t, n + 1); if (buf == NULL) { return PyErr_NoMemory(); } - if (use_xy) { - rtn = mvwinnwstr(self->win, y, x, buf, n); - } - else { - rtn = winnwstr(self->win, buf, n); + /* Read again if the library stored more than asked: truncating could + separate a combining character from its base. */ + for (unsigned int want = n - CURSES_STR_EXTRA; ; n = want) { + if (use_xy) { + rtn = mvwinnwstr(self->win, y, x, buf, n); + } + else { + rtn = winnwstr(self->win, buf, n); + } + if (rtn == ERR || (unsigned int)rtn <= want) { + break; + } } if (rtn == ERR) { @@ -4003,7 +4027,8 @@ PyCursesWindow_in_wchstr(PyObject *op, PyObject *args) return NULL; } - n = Py_MIN(n, max_buf_size - 1); + n = Py_MIN(n, max_buf_size - 1 - CURSES_STR_EXTRA); + n += CURSES_STR_EXTRA; cursesmodule_state *state = get_cursesmodule_state_by_win(self); /* Zero the cells: reading a cell back through getcchar() relies on the cchar_t text array being NUL-terminated, which some curses libraries @@ -4026,6 +4051,7 @@ PyCursesWindow_in_wchstr(PyObject *op, PyObject *args) return PyCursesComplexStr_New(state, NULL, 0); } + n -= CURSES_STR_EXTRA; /* win_wchnstr() stores at most n cells and zero-terminates the array at the actual count; every real cell holds at least a space, so the first empty cell marks the end of the run. */ @@ -4058,6 +4084,7 @@ PyCursesWindow_in_wchstr(PyObject *op, PyObject *args) return PyCursesComplexStr_New(state, NULL, 0); } + n -= CURSES_STR_EXTRA; Py_ssize_t count = 0; while (count < (Py_ssize_t)n && buf[count] != 0) { count++; From 3e6622a3afe5a91ccd2f7c338b1ec7841f04ac25 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 29 Jul 2026 14:20:55 +0300 Subject: [PATCH 2/2] Add a NEWS entry --- .../Library/2026-07-29-14-20-40.gh-issue-154855.vfF453.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-14-20-40.gh-issue-154855.vfF453.rst diff --git a/Misc/NEWS.d/next/Library/2026-07-29-14-20-40.gh-issue-154855.vfF453.rst b/Misc/NEWS.d/next/Library/2026-07-29-14-20-40.gh-issue-154855.vfF453.rst new file mode 100644 index 00000000000000..c92cfaa4a72953 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-14-20-40.gh-issue-154855.vfF453.rst @@ -0,0 +1,4 @@ +Fix :meth:`curses.window.instr`, :meth:`~curses.window.in_wstr` and +:meth:`~curses.window.in_wchstr` returning one character too few when the +:mod:`curses` module is built against a curses library that counts the +terminator in the requested length, such as the NetBSD one.