Skip to content

Commit ca79981

Browse files
gh-156230: Bound a curses window read by the window, not by 2047 (GH-156282)
instr(), in_wstr() and in_wchstr() clamped the count to 2047 and silently truncated a longer line, which a pad can have. A window read cannot return more than the columns left on the line, in the unit each method counts: cells, characters, or bytes at CCHARW_MAX characters of MB_CUR_MAX bytes per cell. Cap the count by that, and read the rest of the line when the count is omitted, which it now can be. getstr() and get_wstr() read the keyboard rather than the window, so their limit stays.
1 parent 2c47c65 commit ca79981

5 files changed

Lines changed: 208 additions & 110 deletions

File tree

Doc/library/curses.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,26 +1404,29 @@ Reading window contents
14041404
window.instr(y, x[, n])
14051405

14061406
Read the text of the window from the current cursor position,
1407-
or from *y*, *x* if specified, to the end of the line,
1407+
or from *y*, *x* if specified, to the end of the line
1408+
or at most *n* bytes if *n* is specified,
14081409
and return it as a bytes object, in the encoding of the current locale.
14091410
Attributes and color pairs are stripped;
14101411
use :meth:`in_wchstr` to read them too.
1411-
At most *n* bytes are read; *n* defaults to and cannot exceed 2047.
14121412
A character not representable in the encoding cannot be returned;
14131413
use :meth:`in_wstr` for those.
14141414

14151415
.. versionchanged:: 3.14
14161416
The maximum value for *n* was increased from 1023 to 2047.
14171417

1418+
.. versionchanged:: next
1419+
*n* is no longer limited to 2047.
1420+
14181421
.. method:: window.in_wstr([n])
14191422
window.in_wstr(y, x[, n])
14201423

14211424
Read the text of the window from the current cursor position,
1422-
or from *y*, *x* if specified, to the end of the line,
1425+
or from *y*, *x* if specified, to the end of the line
1426+
or at most *n* characters if *n* is specified,
14231427
and return it as a :class:`str`.
14241428
Attributes and color pairs are stripped;
14251429
use :meth:`in_wchstr` to read them too.
1426-
At most *n* characters are read; *n* defaults to and cannot exceed 2047.
14271430

14281431
This is the wide-character variant of :meth:`instr`.
14291432

@@ -1433,12 +1436,12 @@ Reading window contents
14331436
window.in_wchstr(y, x[, n])
14341437

14351438
Read the styled cells of the window from the current cursor position,
1436-
or from *y*, *x* if specified, to the end of the line,
1439+
or from *y*, *x* if specified, to the end of the line
1440+
or at most *n* cells if *n* is specified,
14371441
and return them as a :class:`complexstr`.
14381442
Unlike :meth:`instr` and :meth:`in_wstr`, each cell keeps its attributes
14391443
and color pair, so the result can be written back unchanged
14401444
with :meth:`addstr`.
1441-
At most *n* cells are read; *n* defaults to and cannot exceed 2047.
14421445

14431446
.. versionadded:: next
14441447

Lib/test/test_curses.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,11 @@ def test_in_wstr(self):
672672
stdscr.addstr(0, 0, 'abz')
673673
self.assertEqual(stdscr.in_wstr(0, 0, 0), '')
674674
self.assertEqual(stdscr.in_wstr(0), '')
675+
self.assertEqual(stdscr.in_wstr(0, 0, 2**31), stdscr.in_wstr(0, 0))
676+
self.assertRaises(OverflowError, stdscr.in_wstr, 2**1000)
677+
self.assertRaises(ValueError, stdscr.in_wstr, -2)
678+
self.assertRaises(ValueError, stdscr.in_wstr, 0, 2, -2)
679+
self.assertRaises(ValueError, stdscr.in_wstr, -2**1000)
675680

676681
def test_complexchar(self):
677682
# A complexchar is a styled wide-character cell: str() is its text,
@@ -871,6 +876,11 @@ def test_in_wchstr(self):
871876
# The count is optional and reads to the end of the line by default.
872877
stdscr.move(0, 0)
873878
self.assertEqual(str(stdscr.in_wchstr())[:3], 'AbC')
879+
self.assertEqual(stdscr.in_wchstr(0, 0, 2**31), stdscr.in_wchstr(0, 0))
880+
self.assertRaises(OverflowError, stdscr.in_wchstr, 2**1000)
881+
self.assertRaises(ValueError, stdscr.in_wchstr, -2)
882+
self.assertRaises(ValueError, stdscr.in_wchstr, 0, 2, -2)
883+
self.assertRaises(ValueError, stdscr.in_wchstr, -2**1000)
874884

875885
def test_complexstr_in_write_methods(self):
876886
# addstr/addnstr/insstr/insnstr also accept a complexstr, written via
@@ -1188,8 +1198,13 @@ def test_read_from_window(self):
11881198
self.assertEqual(stdscr.instr(3)[:6], b' AB')
11891199
self.assertEqual(stdscr.instr(0, 2)[:4], b'BCD ')
11901200
self.assertEqual(stdscr.instr(0, 2, 4), b'BCD ')
1201+
# A huge count is bounded by the line, and is not used to size the
1202+
# read buffer.
1203+
self.assertEqual(stdscr.instr(0, 0, 2**31), stdscr.instr(0, 0))
1204+
self.assertRaises(OverflowError, stdscr.instr, 2**1000)
11911205
self.assertRaises(ValueError, stdscr.instr, -2)
11921206
self.assertRaises(ValueError, stdscr.instr, 0, 2, -2)
1207+
self.assertRaises(ValueError, stdscr.instr, -2**1000)
11931208
# instr(y, x, 1) reads a single cell byte, so only a character that the
11941209
# window encoding maps to one byte is checked. inch() returns the cell
11951210
# value, which is the locale byte.
@@ -1206,6 +1221,25 @@ def test_read_from_window(self):
12061221
self.assertEqual(stdscr.instr(2, 0, 1), b)
12071222
self.assertEqual(stdscr.inch(2, 0), v)
12081223

1224+
def test_read_long_line(self):
1225+
# A pad line can be longer than a window, and a character can be
1226+
# encoded with several bytes, so instr() can read more bytes than
1227+
# there are cells. See _encodable for the character set.
1228+
width = 3000
1229+
pad = curses.newpad(1, width)
1230+
for ch in ['z', '\u00e9', '\u20ac', '\u0434', '\uff71']:
1231+
if not self._storable(ch):
1232+
continue
1233+
pad.addstr(0, 0, ch)
1234+
if pad.getyx()[1] != 1:
1235+
continue # a wide character occupies two cells
1236+
with self.subTest(ch=ch):
1237+
line = ch * (width - 1) + ' ' # the last cell is left blank
1238+
pad.addstr(0, 0, line[:-1])
1239+
self.assertEqual(pad.instr(0, 0), line.encode(pad.encoding))
1240+
self.assertEqual(pad.in_wstr(0, 0), line)
1241+
self.assertEqual(str(pad.in_wchstr(0, 0)), line)
1242+
12091243
def test_coordinate_errors(self):
12101244
# Addressing a cell outside the window raises curses.error.
12111245
win = curses.newwin(5, 10, 0, 0)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`curses.window.instr`, :meth:`~curses.window.in_wstr` and
2+
:meth:`~curses.window.in_wchstr` no longer limit the count to 2047, which
3+
silently truncated a longer line.

0 commit comments

Comments
 (0)