Skip to content

Commit 8285a2a

Browse files
committed
gh-156230: Bound curses window reads by the window instead of 2047
1 parent f74cdf8 commit 8285a2a

4 files changed

Lines changed: 156 additions & 69 deletions

File tree

Doc/library/curses.rst

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,8 @@ Reading input
12991299
Read a bytes object from the user, with primitive line editing capacity.
13001300
At most *n* characters are read;
13011301
*n* defaults to and cannot exceed 2047.
1302+
Unlike the window readers such as :meth:`instr`, this limit is on keyboard
1303+
input and is not derived from the window geometry.
13021304
A multibyte character is returned as its encoded bytes; use :meth:`get_wstr`
13031305
to read the input as a :class:`str`.
13041306

@@ -1357,23 +1359,30 @@ Reading window contents
13571359
Return a bytes object of characters, extracted from the window starting at the
13581360
current cursor position, or at *y*, *x* if specified, and stopping at the end
13591361
of the line. Attributes and color information are stripped
1360-
from the characters. If *n* is specified, :meth:`instr` returns a string
1361-
at most *n* characters long (exclusive of the trailing NUL).
1362-
The maximum value for *n* is 2047.
1362+
from the characters. If *n* is specified and nonnegative, :meth:`instr`
1363+
returns a string at most *n* characters long (exclusive of the trailing NUL);
1364+
by default, or if *n* is negative, the rest of the line is returned.
13631365
A character not representable in the window's encoding cannot be returned;
13641366
use :meth:`in_wstr` for those.
13651367

13661368
.. versionchanged:: 3.14
13671369
The maximum value for *n* was increased from 1023 to 2047.
13681370

1371+
.. versionchanged:: next
1372+
The 2047 limit was removed: the read is bounded by the window, so a line
1373+
longer than 2047 characters is no longer silently truncated. *n* now
1374+
defaults to ``-1``, meaning the rest of the line.
1375+
13691376
.. method:: window.in_wstr([n])
13701377
window.in_wstr(y, x[, n])
13711378

13721379
Return a string of characters, extracted from the window starting at the
1373-
current cursor position, or at *y*, *x* if specified. Unlike :meth:`instr`,
1374-
it can return characters that are not representable in the window's encoding.
1375-
Attributes and color information are stripped from the characters. The
1376-
maximum value for *n* is 2047.
1380+
current cursor position, or at *y*, *x* if specified, and stopping at the end
1381+
of the line. Unlike :meth:`instr`, it can return characters that are not
1382+
representable in the window's encoding. Attributes and color information are
1383+
stripped from the characters. If *n* is specified and nonnegative, at most *n*
1384+
characters are returned; by default, or if *n* is negative, the rest of the line
1385+
is returned.
13771386

13781387
.. versionadded:: next
13791388

@@ -1384,8 +1393,9 @@ Reading window contents
13841393
starting at the current cursor position, or at *y*, *x* if specified, and
13851394
stopping at the end of the line. This is the variant of :meth:`instr` and
13861395
:meth:`in_wstr` that *keeps* each cell's attributes and color pair (those
1387-
methods strip the rendition). If *n* is specified, at most *n* cells are
1388-
returned. The maximum value for *n* is 2047.
1396+
methods strip the rendition). If *n* is specified and nonnegative, at most
1397+
*n* cells are returned; by default, or if *n* is negative, the rest of the
1398+
line is returned.
13891399

13901400
The result can be written back unchanged with :meth:`addstr` (a read and a
13911401
re-write is a round-trip that preserves every cell's rendition).

Lib/test/test_curses.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,8 +1188,9 @@ def test_read_from_window(self):
11881188
self.assertEqual(stdscr.instr(3)[:6], b' AB')
11891189
self.assertEqual(stdscr.instr(0, 2)[:4], b'BCD ')
11901190
self.assertEqual(stdscr.instr(0, 2, 4), b'BCD ')
1191-
self.assertRaises(ValueError, stdscr.instr, -2)
1192-
self.assertRaises(ValueError, stdscr.instr, 0, 2, -2)
1191+
# A negative n means the rest of the line, as it does for chgat().
1192+
self.assertEqual(stdscr.instr(-2), stdscr.instr())
1193+
self.assertEqual(stdscr.instr(0, 2, -2), stdscr.instr(0, 2))
11931194
# instr(y, x, 1) reads a single cell byte, so only a character that the
11941195
# window encoding maps to one byte is checked. inch() returns the cell
11951196
# value, which is the locale byte.
@@ -1206,6 +1207,27 @@ def test_read_from_window(self):
12061207
self.assertEqual(stdscr.instr(2, 0, 1), b)
12071208
self.assertEqual(stdscr.inch(2, 0), v)
12081209

1210+
def test_read_wide_pad(self):
1211+
# gh-156230: a window read is bounded by the window, not by a fixed
1212+
# buffer size, so a pad wider than 2047 columns is not truncated.
1213+
width = 3000
1214+
pad = curses.newpad(1, width)
1215+
pad.addstr(0, 2500, 'X')
1216+
1217+
line = pad.instr(0, 0)
1218+
self.assertEqual(len(line), width)
1219+
self.assertEqual(line[2500:2501], b'X')
1220+
# The count is clamped to the columns left on the line, and an
1221+
# explicit count still limits the read.
1222+
self.assertEqual(len(pad.instr(0, 2900)), 100)
1223+
self.assertEqual(len(pad.instr(0, 0, 5)), 5)
1224+
self.assertEqual(len(pad.instr(0, 0, width + 1000)), width)
1225+
1226+
self.assertEqual(len(pad.in_wstr(0, 0)), width)
1227+
self.assertEqual(pad.in_wstr(0, 0)[2500], 'X')
1228+
self.assertEqual(len(pad.in_wchstr(0, 0)), width)
1229+
self.assertEqual(str(pad.in_wchstr(0, 2500, 1)), 'X')
1230+
12091231
def test_coordinate_errors(self):
12101232
# Addressing a cell outside the window raises curses.error.
12111233
win = curses.newwin(5, 10, 0, 0)

Modules/_cursesmodule.c

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3801,28 +3801,82 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1,
38013801
return PyLong_FromUnsignedLongLong(rtn);
38023802
}
38033803

3804+
/* Maximum number of characters, and of bytes, that one cell can hold: a
3805+
spacing character optionally followed by CCHARW_MAX -1 combining
3806+
characters, each encoding to at most MB_CUR_MAX bytes. A narrow build
3807+
stores a single byte per call. */
3808+
#ifdef HAVE_NCURSESW
3809+
# define CURSES_CELL_CHARS ((size_t)CCHARW_MAX)
3810+
# define CURSES_CELL_BYTES (CURSES_CELL_CHARS * (size_t)MB_CUR_MAX)
3811+
#else
3812+
# define CURSES_CELL_CHARS ((size_t)1)
3813+
# define CURSES_CELL_BYTES ((size_t)1)
3814+
#endif
3815+
3816+
/* Number of units that can be read from the read position to the end of the
3817+
line, where units_per_cell is what the caller counts per cell: 1 for cells,
3818+
CURSES_CELL_CHARS for wide characters, CURSES_CELL_BYTES for bytes.
3819+
Return 0 if the position lies outside the window: the curses call then fails
3820+
and the caller returns an empty result. */
3821+
static size_t
3822+
curses_window_line_size(PyCursesWindowObject *self, int use_xy, int y, int x,
3823+
size_t units_per_cell)
3824+
{
3825+
int cury, curx, maxy, maxx;
3826+
3827+
getmaxyx(self->win, maxy, maxx);
3828+
if (use_xy) {
3829+
cury = y;
3830+
curx = x;
3831+
}
3832+
else {
3833+
getyx(self->win, cury, curx);
3834+
}
3835+
if (cury < 0 || cury >= maxy || curx < 0 || curx >= maxx) {
3836+
return 0;
3837+
}
3838+
size_t cells = (size_t)maxx - (size_t)curx;
3839+
if (cells > (size_t)INT_MAX / units_per_cell) {
3840+
return (size_t)INT_MAX;
3841+
}
3842+
return cells * units_per_cell;
3843+
}
3844+
3845+
/* Clamp the requested count to what is left on the line; a negative count
3846+
means the whole rest of the line. */
3847+
static size_t
3848+
curses_window_read_size(PyCursesWindowObject *self, int use_xy, int y, int x,
3849+
int n, size_t units_per_cell)
3850+
{
3851+
size_t max_size = curses_window_line_size(self, use_xy, y, x,
3852+
units_per_cell);
3853+
return n < 0 ? max_size : Py_MIN((size_t)n, max_size);
3854+
}
3855+
38043856
/* Extract characters from the window into a new bytes object (empty on ERR),
38053857
with attributes and color stripped. Shared by instr() and, without the wide
38063858
library, by in_wstr(). */
38073859
static PyObject *
38083860
curses_window_instr_bytes(PyCursesWindowObject *self, int use_xy,
3809-
int y, int x, unsigned int n)
3861+
int y, int x, int n)
38103862
{
38113863
int rtn;
3812-
unsigned int max_buf_size = 2048;
3864+
size_t size = curses_window_read_size(self, use_xy, y, x, n,
3865+
CURSES_CELL_BYTES);
38133866

3814-
n = Py_MIN(n, max_buf_size - 1);
3815-
PyBytesWriter *writer = PyBytesWriter_Create(n + 1);
3867+
PyBytesWriter *writer = PyBytesWriter_Create((Py_ssize_t)size + 1);
38163868
if (writer == NULL) {
38173869
return NULL;
38183870
}
38193871
char *buf = PyBytesWriter_GetData(writer);
3872+
/* curses does not terminate the buffer when nothing is read. */
3873+
buf[0] = '\0';
38203874

38213875
if (use_xy) {
3822-
rtn = mvwinnstr(self->win, y, x, buf, n);
3876+
rtn = mvwinnstr(self->win, y, x, buf, (int)size);
38233877
}
38243878
else {
3825-
rtn = winnstr(self->win, buf, n);
3879+
rtn = winnstr(self->win, buf, (int)size);
38263880
}
38273881

38283882
if (rtn == ERR) {
@@ -3841,24 +3895,25 @@ _curses.window.instr
38413895
x: int
38423896
X-coordinate.
38433897
]
3844-
n: unsigned_int = 2047
3845-
Maximal number of characters.
3898+
n: int = -1
3899+
Maximal number of characters; negative means to the end of the line.
38463900
/
38473901
38483902
Return a string of characters, extracted from the window.
38493903
38503904
Return a string of characters, extracted from the window starting
38513905
at the current cursor position, or at y, x if specified, and
38523906
stopping at the end of the line. Attributes and color
3853-
information are stripped from the characters. If n is specified,
3854-
instr() returns a string at most n characters long (exclusive of
3855-
the trailing NUL).
3907+
information are stripped from the characters. If n is specified
3908+
and nonnegative, instr() returns a string at most n characters
3909+
long (exclusive of the trailing NUL); by default, or if n is
3910+
negative, the rest of the line is returned.
38563911
[clinic start generated code]*/
38573912

38583913
static PyObject *
38593914
_curses_window_instr_impl(PyCursesWindowObject *self, int group_left_1,
3860-
int y, int x, unsigned int n)
3861-
/*[clinic end generated code: output=40081f67070132da input=85e62048d2d92642]*/
3915+
int y, int x, int n)
3916+
/*[clinic end generated code: output=55246cfe039301bb input=6c052d79ef35ce11]*/
38623917
{
38633918
return curses_window_instr_bytes(self, group_left_1, y, x, n);
38643919
}
@@ -3956,8 +4011,8 @@ _curses.window.in_wstr
39564011
x: int
39574012
X-coordinate.
39584013
]
3959-
n: unsigned_int = 2047
3960-
Maximal number of characters.
4014+
n: int = -1
4015+
Maximal number of characters; negative means to the end of the line.
39614016
/
39624017
39634018
Return a string of characters, extracted from the window.
@@ -3967,24 +4022,24 @@ This is the wide-character variant of instr(); it returns a str.
39674022

39684023
static PyObject *
39694024
_curses_window_in_wstr_impl(PyCursesWindowObject *self, int group_left_1,
3970-
int y, int x, unsigned int n)
3971-
/*[clinic end generated code: output=e3db72a1f10b9875 input=196703989dc57361]*/
4025+
int y, int x, int n)
4026+
/*[clinic end generated code: output=5137f0184e742a46 input=5fb03546802762cb]*/
39724027
{
39734028
#ifdef HAVE_NCURSESW
39744029
int rtn;
3975-
unsigned int max_buf_size = 2048;
4030+
size_t size = curses_window_read_size(self, group_left_1, y, x, n,
4031+
CURSES_CELL_CHARS);
39764032

3977-
n = Py_MIN(n, max_buf_size - 1);
3978-
wchar_t *buf = PyMem_New(wchar_t, n + 1);
4033+
wchar_t *buf = PyMem_New(wchar_t, size + 1);
39794034
if (buf == NULL) {
39804035
return PyErr_NoMemory();
39814036
}
39824037

39834038
if (group_left_1) {
3984-
rtn = mvwinnwstr(self->win, y, x, buf, n);
4039+
rtn = mvwinnwstr(self->win, y, x, buf, (int)size);
39854040
}
39864041
else {
3987-
rtn = winnwstr(self->win, buf, n);
4042+
rtn = winnwstr(self->win, buf, (int)size);
39884043
}
39894044

39904045
if (rtn == ERR) {
@@ -4018,8 +4073,8 @@ _curses.window.in_wchstr
40184073
x: int
40194074
X-coordinate.
40204075
]
4021-
n: unsigned_int = 2047
4022-
Maximal number of cells.
4076+
n: int = -1
4077+
Maximal number of cells; negative means to the end of the line.
40234078
/
40244079
40254080
Return a complexstr of the styled cells extracted from the window.
@@ -4031,28 +4086,27 @@ complexstr.
40314086

40324087
static PyObject *
40334088
_curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
4034-
int y, int x, unsigned int n)
4035-
/*[clinic end generated code: output=7fb5216f2088835b input=b725c0b8abff62c2]*/
4089+
int y, int x, int n)
4090+
/*[clinic end generated code: output=ce23cd8f85982884 input=8a1138d6fae0e389]*/
40364091
{
40374092
int rtn;
4038-
unsigned int max_buf_size = 2048;
4093+
size_t size = curses_window_read_size(self, group_left_1, y, x, n, 1);
40394094

4040-
n = Py_MIN(n, max_buf_size - 1);
40414095
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
40424096
/* Zero the cells: reading a cell back through getcchar() relies on the
40434097
cchar_t text array being NUL-terminated, which some curses libraries
40444098
only guarantee for the characters they actually write. */
4045-
curses_cell_t *buf = PyMem_Calloc(n + 1, sizeof(curses_cell_t));
4099+
curses_cell_t *buf = PyMem_Calloc(size + 1, sizeof(curses_cell_t));
40464100
if (buf == NULL) {
40474101
return PyErr_NoMemory();
40484102
}
40494103

40504104
#ifdef HAVE_NCURSESW
40514105
if (group_left_1) {
4052-
rtn = mvwin_wchnstr(self->win, y, x, buf, n);
4106+
rtn = mvwin_wchnstr(self->win, y, x, buf, (int)size);
40534107
}
40544108
else {
4055-
rtn = win_wchnstr(self->win, buf, n);
4109+
rtn = win_wchnstr(self->win, buf, (int)size);
40564110
}
40574111

40584112
if (rtn == ERR) {
@@ -4064,7 +4118,7 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
40644118
the actual count; every real cell holds at least a space, so the first
40654119
empty cell marks the end of the run. */
40664120
Py_ssize_t count = 0;
4067-
while (count < (Py_ssize_t)n) {
4121+
while (count < (Py_ssize_t)size) {
40684122
wchar_t wstr[CCHARW_MAX + 1];
40694123
attr_t attrs;
40704124
int pair;
@@ -4079,12 +4133,12 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
40794133
/* winchnstr() is not guaranteed (SVr4) to terminate the array, so pre-zero
40804134
it and stop at the first empty cell; a painted cell always holds at least
40814135
a space, never 0. */
4082-
memset(buf, 0, ((size_t)n + 1) * sizeof(curses_cell_t));
4136+
memset(buf, 0, (size + 1) * sizeof(curses_cell_t));
40834137
if (group_left_1) {
4084-
rtn = mvwinchnstr(self->win, y, x, buf, n);
4138+
rtn = mvwinchnstr(self->win, y, x, buf, (int)size);
40854139
}
40864140
else {
4087-
rtn = winchnstr(self->win, buf, n);
4141+
rtn = winchnstr(self->win, buf, (int)size);
40884142
}
40894143

40904144
if (rtn == ERR) {
@@ -4093,7 +4147,7 @@ _curses_window_in_wchstr_impl(PyCursesWindowObject *self, int group_left_1,
40934147
}
40944148

40954149
Py_ssize_t count = 0;
4096-
while (count < (Py_ssize_t)n && buf[count] != 0) {
4150+
while (count < (Py_ssize_t)size && buf[count] != 0) {
40974151
count++;
40984152
}
40994153
#endif

0 commit comments

Comments
 (0)