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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/7d7fa9d73b9e6e0cd14a34e7953010f80aa290d4
commit: 7d7fa9d73b9e6e0cd14a34e7953010f80aa290d4
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-23T13:07:20Z
summary:

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

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() is left as it is.

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.

(cherry picked from commit 43a1869f7eea006f04647d4225d5fda80cb3fbd9)

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..d8895e32bafe8a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-29-14-20-40.gh-issue-154855.vfF453.rst
@@ -0,0 +1,3 @@
+Fix :meth:`curses.window.instr` 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 fa3c6cda76d779..e1c26ad7f06bc9 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -185,6 +185,14 @@ static int initialisedcolors = FALSE;
    while these functions are still in use. */
 static char *screen_encoding = NULL;
 
+/* 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 Macros */
 #define PyCursesSetupTermCalled                                         \
     if (initialised_setupterm != TRUE) {                                \
@@ -1839,13 +1847,13 @@ bytes are read.
 static PyObject *
 PyCursesWindow_InStr(PyCursesWindowObject *self, PyObject *args)
 {
-    int x, y, n;
+    int x = 0, y = 0, n;
     char rtn[1024]; /* This should be big enough.. I hope */
-    int rtn2;
+    int rtn2, use_xy = 0;
 
     switch (PyTuple_Size(args)) {
     case 0:
-        rtn2 = winnstr(self->win,rtn, 1023);
+        n = 1023;
         break;
     case 1:
         if (!PyArg_ParseTuple(args,"i;n", &n))
@@ -1854,12 +1862,13 @@ PyCursesWindow_InStr(PyCursesWindowObject *self, PyObject *args)
             PyErr_SetString(PyExc_ValueError, "'n' must be nonnegative");
             return NULL;
         }
-        rtn2 = winnstr(self->win, rtn, Py_MIN(n, 1023));
+        n = Py_MIN(n, 1023);
         break;
     case 2:
         if (!PyArg_ParseTuple(args,"ii;y,x",&y,&x))
             return NULL;
-        rtn2 = mvwinnstr(self->win,y,x,rtn,1023);
+        n = 1023;
+        use_xy = 1;
         break;
     case 3:
         if (!PyArg_ParseTuple(args, "iii;y,x,n", &y, &x, &n))
@@ -1868,15 +1877,31 @@ PyCursesWindow_InStr(PyCursesWindowObject *self, PyObject *args)
             PyErr_SetString(PyExc_ValueError, "'n' must be nonnegative");
             return NULL;
         }
-        rtn2 = mvwinnstr(self->win, y, x, rtn, Py_MIN(n,1023));
+        n = Py_MIN(n, 1023);
+        use_xy = 1;
         break;
     default:
         PyErr_SetString(PyExc_TypeError, "instr requires 0 or 3 arguments");
         return NULL;
     }
+
+    /* Read again if the library stored more than asked: truncating could
+       split a multibyte character. */
+    for (int ask = n + CURSES_STR_EXTRA; ; ask = n) {
+        if (use_xy) {
+            rtn2 = mvwinnstr(self->win, y, x, rtn, ask);
+        }
+        else {
+            rtn2 = winnstr(self->win, rtn, ask);
+        }
+        if (rtn2 == ERR || rtn2 <= n) {
+            break;
+        }
+    }
+
     if (rtn2 == ERR)
-        rtn[0] = 0;
-    return PyBytes_FromString(rtn);
+        return PyBytes_FromStringAndSize(NULL, 0);
+    return PyBytes_FromStringAndSize(rtn, rtn2);
 }
 
 /*[clinic input]

_______________________________________________
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.