diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 6a7a8e320438f8..ca22a4080d78b1 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1017,6 +1017,37 @@ def test_output_string_pair_restored(self): curses.A_BOLD) self.assertEqual(win.attr_get()[1], pair) + def test_cell_embedded_null_chars(self): + # A NUL cannot share a cell with another character: setcchar() takes a + # NUL-terminated string, so the rest of the cell would be dropped. + for text in ['a\0', 'a\0\u0301', 'a\0b', '\0a']: + with self.subTest(text=text): + self.assertRaises(ValueError, curses.complexchar, text) + if WIDE_BUILD: + self.assertRaises(ValueError, self.stdscr.addch, text) + + def test_cell_null_char(self): + # A lone NUL is a character like any other, as addch(0) always was. + stdscr = self.stdscr + cell = curses.complexchar('\0') + self.assertEqual(str(cell), '\0') + self.assertEqual(eval(repr(cell), {'curses': curses}), cell) + stdscr.erase() + stdscr.addch(0, 0, 0) + expected = stdscr.instr(0, 0, 4) + for ch in ['\0', cell]: + with self.subTest(ch=ch): + stdscr.erase() + stdscr.addch(0, 0, ch) + self.assertEqual(stdscr.instr(0, 0, 4), expected) + # A cell holding a NUL reads back as the cell that writes it. + win = curses.newwin(3, 8, 0, 0) + win.insch(0, 0, '\0') + self.assertEqual(win.in_wch(0, 0), cell) + # A string of cells cannot hold a NUL: it would end a batch write. + self.assertRaises(ValueError, curses.complexstr, 'a\0b') + self.assertRaises(ValueError, curses.complexstr, '\0') + def test_add_string_behavior(self): # addstr() advances the cursor past the written text; addnstr() # writes at most n characters. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index ea399e25213aef..17c8cc79bd6d3e 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -561,6 +561,10 @@ PyCurses_ConvertToWideCell(PyObject *obj, wchar_t *wch) setcchar() would silently drop a trailing spacing character, or fail with a generic error for a control-character base. */ if (nch > 1) { + if (wmemchr(wch, L'\0', nch) != NULL) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + return -1; + } int bad = wcwidth(wch[0]) < 0; for (Py_ssize_t i = 1; !bad && i < nch; i++) { bad = wcwidth(wch[i]) != 0; @@ -908,7 +912,9 @@ curses_cell_text(cursesmodule_state *state, const curses_cell_t *cell) PyErr_SetString(state->error, "getcchar() returned ERR"); return NULL; } - return PyUnicode_FromWideChar(wstr, -1); + /* setcchar() stores no text for a NUL (it takes a NUL-terminated string), + so an empty cell holds a NUL character, as on a narrow build. */ + return PyUnicode_FromWideChar(wstr, wstr[0] == L'\0' ? 1 : -1); #else char ch = (char)(*cell & A_CHARTEXT); return PyUnicode_Decode(&ch, 1, curses_screen_encoding, NULL); @@ -1318,6 +1324,16 @@ static PyObject * complexstr_from_string(cursesmodule_state *state, PyObject *str, attr_t attr, int pair) { + /* A NUL cell ends a batch write and a cell array read (add_wchnstr(3X)), + so a string of cells cannot hold one, as addstr() cannot either. */ + Py_ssize_t nul = PyUnicode_FindChar(str, 0, 0, PyUnicode_GET_LENGTH(str), 1); + if (nul < -1) { + return NULL; + } + if (nul >= 0) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + return NULL; + } #ifdef HAVE_NCURSESW Py_ssize_t n; wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n);