gh-154855: Ask non-ncurses curses for one more character (GH-154870)

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/43a1869f7eea006f04647d4225d5fda80cb3fbd9
commit: 43a1869f7eea006f04647d4225d5fda80cb3fbd9
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-23T12:31:51Z
summary:

gh-154855: Ask non-ncurses curses for one more character (GH-154870)

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.

files:
A Misc/NEWS.d/next/Library/2026-07-29-14-20-40.gh-issue-154855.vfF453.rst
M Modules/_cursesmodule.c

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.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 82effccb1fe327..45ba6476bbc4a6 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 */
 
 /*
@@ -3826,25 +3834,33 @@ curses_window_instr_bytes(PyCursesWindowObject *self, int use_xy,
     int rtn;
     unsigned int max_buf_size = 2048;
 
-    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);
 }
 
 /*[clinic input]
@@ -3992,17 +4008,25 @@ _curses_window_in_wstr_impl(PyCursesWindowObject *self, int group_left_1,
     int rtn;
     unsigned int max_buf_size = 2048;
 
-    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 (group_left_1) {
-        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 (group_left_1) {
+            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) {
@@ -4056,7 +4080,8 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
     int rtn;
     unsigned int max_buf_size = 2048;
 
-    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
@@ -4079,6 +4104,7 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
         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. */
@@ -4111,6 +4137,7 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
         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++;

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.