Skip to content

Commit 43a1869

Browse files
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.
1 parent be87bfa commit 43a1869

2 files changed

Lines changed: 45 additions & 14 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :meth:`curses.window.instr`, :meth:`~curses.window.in_wstr` and
2+
:meth:`~curses.window.in_wchstr` returning one character too few when the
3+
:mod:`curses` module is built against a curses library that counts the
4+
terminator in the requested length, such as the NetBSD one.

Modules/_cursesmodule.c

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,14 @@ curses_window_set_null_error(PyCursesWindowObject *win,
309309
_curses_set_null_error(state, curses_funcname, python_funcname);
310310
}
311311

312+
/* ncurses and PDCurses store n characters and add a terminator; NetBSD
313+
curses counts the terminator in n. Ask an unknown library for one more. */
314+
#if defined(NCURSES_VERSION) || defined(PDCURSES)
315+
# define CURSES_STR_EXTRA 0
316+
#else
317+
# define CURSES_STR_EXTRA 1
318+
#endif
319+
312320
/* Utility Checking Procedures */
313321

314322
/*
@@ -3826,25 +3834,33 @@ curses_window_instr_bytes(PyCursesWindowObject *self, int use_xy,
38263834
int rtn;
38273835
unsigned int max_buf_size = 2048;
38283836

3829-
n = Py_MIN(n, max_buf_size - 1);
3837+
n = Py_MIN(n, max_buf_size - 1 - CURSES_STR_EXTRA);
3838+
n += CURSES_STR_EXTRA;
38303839
PyBytesWriter *writer = PyBytesWriter_Create(n + 1);
38313840
if (writer == NULL) {
38323841
return NULL;
38333842
}
38343843
char *buf = PyBytesWriter_GetData(writer);
38353844

3836-
if (use_xy) {
3837-
rtn = mvwinnstr(self->win, y, x, buf, n);
3838-
}
3839-
else {
3840-
rtn = winnstr(self->win, buf, n);
3845+
/* Read again if the library stored more than asked: truncating could
3846+
split a multibyte character. */
3847+
for (unsigned int want = n - CURSES_STR_EXTRA; ; n = want) {
3848+
if (use_xy) {
3849+
rtn = mvwinnstr(self->win, y, x, buf, n);
3850+
}
3851+
else {
3852+
rtn = winnstr(self->win, buf, n);
3853+
}
3854+
if (rtn == ERR || (unsigned int)rtn <= want) {
3855+
break;
3856+
}
38413857
}
38423858

38433859
if (rtn == ERR) {
38443860
PyBytesWriter_Discard(writer);
38453861
return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
38463862
}
3847-
return PyBytesWriter_FinishWithSize(writer, strlen(buf));
3863+
return PyBytesWriter_FinishWithSize(writer, rtn);
38483864
}
38493865

38503866
/*[clinic input]
@@ -3992,17 +4008,25 @@ _curses_window_in_wstr_impl(PyCursesWindowObject *self, int group_left_1,
39924008
int rtn;
39934009
unsigned int max_buf_size = 2048;
39944010

3995-
n = Py_MIN(n, max_buf_size - 1);
4011+
n = Py_MIN(n, max_buf_size - 1 - CURSES_STR_EXTRA);
4012+
n += CURSES_STR_EXTRA;
39964013
wchar_t *buf = PyMem_New(wchar_t, n + 1);
39974014
if (buf == NULL) {
39984015
return PyErr_NoMemory();
39994016
}
40004017

4001-
if (group_left_1) {
4002-
rtn = mvwinnwstr(self->win, y, x, buf, n);
4003-
}
4004-
else {
4005-
rtn = winnwstr(self->win, buf, n);
4018+
/* Read again if the library stored more than asked: truncating could
4019+
separate a combining character from its base. */
4020+
for (unsigned int want = n - CURSES_STR_EXTRA; ; n = want) {
4021+
if (group_left_1) {
4022+
rtn = mvwinnwstr(self->win, y, x, buf, n);
4023+
}
4024+
else {
4025+
rtn = winnwstr(self->win, buf, n);
4026+
}
4027+
if (rtn == ERR || (unsigned int)rtn <= want) {
4028+
break;
4029+
}
40064030
}
40074031

40084032
if (rtn == ERR) {
@@ -4056,7 +4080,8 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
40564080
int rtn;
40574081
unsigned int max_buf_size = 2048;
40584082

4059-
n = Py_MIN(n, max_buf_size - 1);
4083+
n = Py_MIN(n, max_buf_size - 1 - CURSES_STR_EXTRA);
4084+
n += CURSES_STR_EXTRA;
40604085
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
40614086
/* Zero the cells: reading a cell back through getcchar() relies on the
40624087
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,
40794104
return PyCursesComplexStr_New(state, NULL, 0);
40804105
}
40814106

4107+
n -= CURSES_STR_EXTRA;
40824108
/* win_wchnstr() stores at most n cells and zero-terminates the array at
40834109
the actual count; every real cell holds at least a space, so the first
40844110
empty cell marks the end of the run. */
@@ -4111,6 +4137,7 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
41114137
return PyCursesComplexStr_New(state, NULL, 0);
41124138
}
41134139

4140+
n -= CURSES_STR_EXTRA;
41144141
Py_ssize_t count = 0;
41154142
while (count < (Py_ssize_t)n && buf[count] != 0) {
41164143
count++;

0 commit comments

Comments
 (0)