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]