Skip to content

Commit a4d2907

Browse files
gh-155877: Reject a NUL sharing a curses cell, keep a lone one
setcchar() takes a NUL-terminated string, so a NUL in a cell dropped the rest of it: complexchar('a\0' + combining) kept only 'a', and an empty cell read back as complexchar('') rather than complexchar('\0') as on a narrow build. Reject a NUL among other characters, read an empty cell as a NUL, and give a NUL its own cell in complexstr().
1 parent ca6e733 commit a4d2907

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

Lib/test/test_curses.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,37 @@ def test_output_string_pair_restored(self):
10171017
curses.A_BOLD)
10181018
self.assertEqual(win.attr_get()[1], pair)
10191019

1020+
def test_cell_embedded_null_chars(self):
1021+
# A NUL cannot share a cell with another character: setcchar() takes a
1022+
# NUL-terminated string, so the rest of the cell would be dropped.
1023+
for text in ['a\0', 'a\0\u0301', 'a\0b', '\0a']:
1024+
with self.subTest(text=text):
1025+
self.assertRaises(ValueError, curses.complexchar, text)
1026+
if WIDE_BUILD:
1027+
self.assertRaises(ValueError, self.stdscr.addch, text)
1028+
1029+
def test_cell_null_char(self):
1030+
# A lone NUL is a character like any other, as addch(0) always was.
1031+
stdscr = self.stdscr
1032+
cell = curses.complexchar('\0')
1033+
self.assertEqual(str(cell), '\0')
1034+
self.assertEqual(eval(repr(cell), {'curses': curses}), cell)
1035+
stdscr.erase()
1036+
stdscr.addch(0, 0, 0)
1037+
expected = stdscr.instr(0, 0, 4)
1038+
for ch in ['\0', cell]:
1039+
with self.subTest(ch=ch):
1040+
stdscr.erase()
1041+
stdscr.addch(0, 0, ch)
1042+
self.assertEqual(stdscr.instr(0, 0, 4), expected)
1043+
# A cell holding a NUL reads back as the cell that writes it.
1044+
win = curses.newwin(3, 8, 0, 0)
1045+
win.insch(0, 0, '\0')
1046+
self.assertEqual(win.in_wch(0, 0), cell)
1047+
# complexstr() splits a NUL into a cell of its own.
1048+
self.assertEqual(len(curses.complexstr('a\0b')), 3)
1049+
self.assertEqual(curses.complexstr('\0')[0], cell)
1050+
10201051
def test_add_string_behavior(self):
10211052
# addstr() advances the cursor past the written text; addnstr()
10221053
# writes at most n characters.

Modules/_cursesmodule.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,10 @@ PyCurses_ConvertToWideCell(PyObject *obj, wchar_t *wch)
561561
setcchar() would silently drop a trailing spacing character, or fail
562562
with a generic error for a control-character base. */
563563
if (nch > 1) {
564+
if (wmemchr(wch, L'\0', nch) != NULL) {
565+
PyErr_SetString(PyExc_ValueError, "embedded null character");
566+
return -1;
567+
}
564568
int bad = wcwidth(wch[0]) < 0;
565569
for (Py_ssize_t i = 1; !bad && i < nch; i++) {
566570
bad = wcwidth(wch[i]) != 0;
@@ -908,7 +912,9 @@ curses_cell_text(cursesmodule_state *state, const curses_cell_t *cell)
908912
PyErr_SetString(state->error, "getcchar() returned ERR");
909913
return NULL;
910914
}
911-
return PyUnicode_FromWideChar(wstr, -1);
915+
/* setcchar() stores no text for a NUL (it takes a NUL-terminated string),
916+
so an empty cell holds a NUL character, as on a narrow build. */
917+
return PyUnicode_FromWideChar(wstr, wstr[0] == L'\0' ? 1 : -1);
912918
#else
913919
char ch = (char)(*cell & A_CHARTEXT);
914920
return PyUnicode_Decode(&ch, 1, curses_screen_encoding, NULL);
@@ -1334,7 +1340,9 @@ complexstr_from_string(cursesmodule_state *state, PyObject *str,
13341340
wchar_t cell[CCHARW_MAX + 1];
13351341
Py_ssize_t k = 0;
13361342
cell[k++] = wbuf[i++];
1337-
while (i < n && k < CCHARW_MAX && wcwidth(wbuf[i]) == 0) {
1343+
while (i < n && k < CCHARW_MAX && wbuf[i] != L'\0' &&
1344+
wcwidth(wbuf[i]) == 0)
1345+
{
13381346
cell[k++] = wbuf[i++];
13391347
}
13401348
cell[k] = L'\0';
@@ -1343,7 +1351,7 @@ complexstr_from_string(cursesmodule_state *state, PyObject *str,
13431351
control character (wcwidth < 0) may stand alone but cannot carry
13441352
combining marks. */
13451353
int width = wcwidth(cell[0]);
1346-
if (width == 0 || (k > 1 && width < 0)) {
1354+
if ((width == 0 && cell[0] != L'\0') || (k > 1 && width < 0)) {
13471355
PyErr_Format(PyExc_ValueError,
13481356
"a character cell must be a single spacing character "
13491357
"optionally followed by up to %d combining characters",

0 commit comments

Comments
 (0)