Skip to content

Commit 9c48dfe

Browse files
gh-155877: Fix a NUL in a curses character cell (GH-156158)
setcchar() takes a NUL-terminated string, so a NUL sharing a cell dropped the rest of the cell, and an empty cell read back as complexchar(''). Reject a NUL that shares a cell or that joins a complexstr, and read an empty cell as complexchar('\0'), as a narrow build already does.
1 parent 65e149a commit 9c48dfe

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

Lib/test/test_curses.py

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

1088+
def test_cell_embedded_null_chars(self):
1089+
# A NUL cannot share a cell with another character: setcchar() takes a
1090+
# NUL-terminated string, so the rest of the cell would be dropped.
1091+
for text in ['a\0', 'a\0\u0301', 'a\0b', '\0a']:
1092+
with self.subTest(text=text):
1093+
self.assertRaises(ValueError, curses.complexchar, text)
1094+
if WIDE_BUILD:
1095+
self.assertRaises(ValueError, self.stdscr.addch, text)
1096+
1097+
def test_cell_null_char(self):
1098+
# A lone NUL is a character like any other, as addch(0) always was.
1099+
stdscr = self.stdscr
1100+
cell = curses.complexchar('\0')
1101+
self.assertEqual(str(cell), '\0')
1102+
self.assertEqual(eval(repr(cell), {'curses': curses}), cell)
1103+
stdscr.erase()
1104+
stdscr.addch(0, 0, 0)
1105+
expected = stdscr.instr(0, 0, 4)
1106+
for ch in ['\0', cell]:
1107+
with self.subTest(ch=ch):
1108+
stdscr.erase()
1109+
stdscr.addch(0, 0, ch)
1110+
self.assertEqual(stdscr.instr(0, 0, 4), expected)
1111+
# A cell holding a NUL reads back as the cell that writes it.
1112+
win = curses.newwin(3, 8, 0, 0)
1113+
win.insch(0, 0, '\0')
1114+
self.assertEqual(win.in_wch(0, 0), cell)
1115+
# A string of cells cannot hold a NUL: it would end a batch write.
1116+
self.assertRaises(ValueError, curses.complexstr, 'a\0b')
1117+
self.assertRaises(ValueError, curses.complexstr, '\0')
1118+
10881119
def test_add_string_behavior(self):
10891120
# addstr() advances the cursor past the written text; addnstr()
10901121
# writes at most n characters.

Modules/_cursesmodule.c

Lines changed: 17 additions & 1 deletion
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);
@@ -1318,6 +1324,16 @@ static PyObject *
13181324
complexstr_from_string(cursesmodule_state *state, PyObject *str,
13191325
attr_t attr, int pair)
13201326
{
1327+
/* A NUL cell ends a batch write and a cell array read (add_wchnstr(3X)),
1328+
so a string of cells cannot hold one, as addstr() cannot either. */
1329+
Py_ssize_t nul = PyUnicode_FindChar(str, 0, 0, PyUnicode_GET_LENGTH(str), 1);
1330+
if (nul < -1) {
1331+
return NULL;
1332+
}
1333+
if (nul >= 0) {
1334+
PyErr_SetString(PyExc_ValueError, "embedded null character");
1335+
return NULL;
1336+
}
13211337
#ifdef HAVE_NCURSESW
13221338
Py_ssize_t n;
13231339
wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n);

0 commit comments

Comments
 (0)