bpo-42681: Fix range checks for color and pair numbers in curses (GH-23874)

This commit is contained in:
Serhiy Storchaka 2021-01-03 22:51:11 +02:00 committed by GitHub
parent 7c83eaa536
commit 1470edd613
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 190 additions and 91 deletions

View File

@ -112,14 +112,15 @@ The module :mod:`curses` defines the following functions:
.. function:: color_content(color_number)
Return the intensity of the red, green, and blue (RGB) components in the color
*color_number*, which must be between ``0`` and :const:`COLORS`. Return a 3-tuple,
*color_number*, which must be between ``0`` and ``COLORS - 1``. Return a 3-tuple,
containing the R,G,B values for the given color, which will be between
``0`` (no component) and ``1000`` (maximum amount of component).
.. function:: color_pair(color_number)
.. function:: color_pair(pair_number)
Return the attribute value for displaying text in the specified color. This
Return the attribute value for displaying text in the specified color pair.
Only the first 256 color pairs are supported. This
attribute value can be combined with :const:`A_STANDOUT`, :const:`A_REVERSE`,
and the other :const:`A_\*` attributes. :func:`pair_number` is the counterpart
to this function.
@ -287,7 +288,7 @@ The module :mod:`curses` defines the following functions:
Change the definition of a color, taking the number of the color to be changed
followed by three RGB values (for the amounts of red, green, and blue
components). The value of *color_number* must be between ``0`` and
:const:`COLORS`. Each of *r*, *g*, *b*, must be a value between ``0`` and
`COLORS - 1`. Each of *r*, *g*, *b*, must be a value between ``0`` and
``1000``. When :func:`init_color` is used, all occurrences of that color on the
screen immediately change to the new definition. This function is a no-op on
most terminals; it is active only if :func:`can_change_color` returns ``True``.
@ -300,7 +301,8 @@ The module :mod:`curses` defines the following functions:
color number. The value of *pair_number* must be between ``1`` and
``COLOR_PAIRS - 1`` (the ``0`` color pair is wired to white on black and cannot
be changed). The value of *fg* and *bg* arguments must be between ``0`` and
:const:`COLORS`. If the color-pair was previously initialized, the screen is
``COLORS - 1``, or, after calling :func:`use_default_colors`, ``-1``.
If the color-pair was previously initialized, the screen is
refreshed and all occurrences of that color-pair are changed to the new
definition.
@ -450,7 +452,7 @@ The module :mod:`curses` defines the following functions:
.. function:: pair_content(pair_number)
Return a tuple ``(fg, bg)`` containing the colors for the requested color pair.
The value of *pair_number* must be between ``1`` and ``COLOR_PAIRS - 1``.
The value of *pair_number* must be between ``0`` and ``COLOR_PAIRS - 1``.
.. function:: pair_number(attr)

View File

@ -4,8 +4,7 @@
# This script doesn't actually display anything very coherent. but it
# does call (nearly) every method and function.
#
# Functions not tested: {def,reset}_{shell,prog}_mode, getch(), getstr(),
# init_color()
# Functions not tested: {def,reset}_{shell,prog}_mode, getch(), getstr()
# Only called, not tested: getmouse(), ungetmouse()
#
@ -13,6 +12,7 @@ import os
import string
import sys
import tempfile
import functools
import unittest
from test.support import requires, verbose, SaveSignals
@ -37,6 +37,15 @@ def requires_curses_func(name):
return unittest.skipUnless(hasattr(curses, name),
'requires curses.%s' % name)
def requires_colors(test):
@functools.wraps(test)
def wrapped(self, *args, **kwargs):
if not curses.has_colors():
self.skipTest('requires colors support')
curses.start_color()
test(self, *args, **kwargs)
return wrapped
term = os.environ.get('TERM')
# If newterm was supported we could use it instead of initscr and not exit
@ -48,6 +57,8 @@ class TestCurses(unittest.TestCase):
@classmethod
def setUpClass(cls):
if verbose:
print(f'TERM={term}', file=sys.stderr, flush=True)
# testing setupterm() inside initscr/endwin
# causes terminal breakage
stdout_fd = sys.__stdout__.fileno()
@ -306,31 +317,101 @@ class TestCurses(unittest.TestCase):
curses.use_env(1)
# Functions only available on a few platforms
def test_colors_funcs(self):
if not curses.has_colors():
self.skipTest('requires colors support')
curses.start_color()
curses.init_pair(2, 1,1)
curses.color_content(1)
curses.color_pair(2)
def bad_colors(self):
return (-1, curses.COLORS, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64)
def bad_colors2(self):
return (curses.COLORS, 2**31, 2**63, 2**64)
def bad_pairs(self):
return (-1, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64)
@requires_colors
def test_color_content(self):
self.assertEqual(curses.color_content(curses.COLOR_BLACK), (0, 0, 0))
curses.color_content(0)
curses.color_content(curses.COLORS - 1)
for color in self.bad_colors():
self.assertRaises(ValueError, curses.color_content, color)
@requires_colors
def test_init_color(self):
if not curses.can_change_color:
self.skipTest('cannot change color')
old = curses.color_content(0)
try:
curses.init_color(0, *old)
except curses.error:
self.skipTest('cannot change color (init_color() failed)')
self.addCleanup(curses.init_color, 0, *old)
curses.init_color(0, 0, 0, 0)
self.assertEqual(curses.color_content(0), (0, 0, 0))
curses.init_color(0, 1000, 1000, 1000)
self.assertEqual(curses.color_content(0), (1000, 1000, 1000))
old = curses.color_content(curses.COLORS - 1)
curses.init_color(curses.COLORS - 1, *old)
self.addCleanup(curses.init_color, curses.COLORS - 1, *old)
curses.init_color(curses.COLORS - 1, 0, 500, 1000)
self.assertEqual(curses.color_content(curses.COLORS - 1), (0, 500, 1000))
for color in self.bad_colors():
self.assertRaises(ValueError, curses.init_color, color, 0, 0, 0)
for comp in (-1, 1001):
self.assertRaises(ValueError, curses.init_color, 0, comp, 0, 0)
self.assertRaises(ValueError, curses.init_color, 0, 0, comp, 0)
self.assertRaises(ValueError, curses.init_color, 0, 0, 0, comp)
@requires_colors
def test_pair_content(self):
if not hasattr(curses, 'use_default_colors'):
self.assertEqual(curses.pair_content(0),
(curses.COLOR_WHITE, curses.COLOR_BLACK))
curses.pair_content(0)
curses.pair_content(curses.COLOR_PAIRS - 1)
curses.pair_number(0)
if hasattr(curses, 'use_default_colors'):
curses.use_default_colors()
for pair in self.bad_pairs():
self.assertRaises(ValueError, curses.pair_content, pair)
self.assertRaises(ValueError, curses.color_content, -1)
self.assertRaises(ValueError, curses.color_content, curses.COLORS + 1)
self.assertRaises(ValueError, curses.color_content, -2**31 - 1)
self.assertRaises(ValueError, curses.color_content, 2**31)
self.assertRaises(ValueError, curses.color_content, -2**63 - 1)
self.assertRaises(ValueError, curses.color_content, 2**63 - 1)
self.assertRaises(ValueError, curses.pair_content, -1)
self.assertRaises(ValueError, curses.pair_content, curses.COLOR_PAIRS)
self.assertRaises(ValueError, curses.pair_content, -2**31 - 1)
self.assertRaises(ValueError, curses.pair_content, 2**31)
self.assertRaises(ValueError, curses.pair_content, -2**63 - 1)
self.assertRaises(ValueError, curses.pair_content, 2**63 - 1)
@requires_colors
def test_init_pair(self):
old = curses.pair_content(1)
curses.init_pair(1, *old)
self.addCleanup(curses.init_pair, 1, *old)
curses.init_pair(1, 0, 0)
self.assertEqual(curses.pair_content(1), (0, 0))
curses.init_pair(1, curses.COLORS - 1, curses.COLORS - 1)
self.assertEqual(curses.pair_content(1),
(curses.COLORS - 1, curses.COLORS - 1))
curses.init_pair(curses.COLOR_PAIRS - 1, 2, 3)
self.assertEqual(curses.pair_content(curses.COLOR_PAIRS - 1), (2, 3))
for pair in self.bad_pairs():
self.assertRaises(ValueError, curses.init_pair, pair, 0, 0)
for color in self.bad_colors2():
self.assertRaises(ValueError, curses.init_pair, 1, color, 0)
self.assertRaises(ValueError, curses.init_pair, 1, 0, color)
@requires_colors
def test_color_attrs(self):
for pair in 0, 1, 255:
attr = curses.color_pair(pair)
self.assertEqual(curses.pair_number(attr), pair, attr)
self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair)
self.assertEqual(curses.color_pair(0), 0)
self.assertEqual(curses.pair_number(0), 0)
@requires_curses_func('use_default_colors')
@requires_colors
def test_use_default_colors(self):
self.assertIn(curses.pair_content(0),
((curses.COLOR_WHITE, curses.COLOR_BLACK), (-1, -1)))
curses.use_default_colors()
self.assertEqual(curses.pair_content(0), (-1, -1))
@requires_curses_func('keyname')
def test_keyname(self):

View File

@ -0,0 +1 @@
Fixed range checks for color and pair numbers in :mod:`curses`.

View File

@ -155,10 +155,8 @@ typedef chtype attr_t; /* No attr_t type is available */
#define _CURSES_PAIR_NUMBER_FUNC pair_content
#endif /* _NCURSES_EXTENDED_COLOR_FUNCS */
#define _CURSES_FUNC_NAME_STR(s) #s
#define _CURSES_INIT_COLOR_FUNC_NAME _CURSES_FUNC_NAME_STR(_CURSES_INIT_COLOR_FUNC)
#define _CURSES_INIT_PAIR_FUNC_NAME _CURSES_FUNC_NAME_STR(_CURSES_INIT_PAIR_FUNC)
#define _CURSES_INIT_COLOR_FUNC_NAME Py_STRINGIFY(_CURSES_INIT_COLOR_FUNC)
#define _CURSES_INIT_PAIR_FUNC_NAME Py_STRINGIFY(_CURSES_INIT_PAIR_FUNC)
/*[clinic input]
module _curses
@ -202,18 +200,6 @@ static char *screen_encoding = NULL;
/* Utility Functions */
static inline int
color_pair_to_attr(short color_number)
{
return ((int)color_number << 8);
}
static inline short
attr_to_color_pair(int attr)
{
return (short)((attr & A_COLOR) >> 8);
}
/*
* Check the return code from a curses function and return None
* or raise an exception as appropriate. These are exported using the
@ -414,7 +400,7 @@ PyCurses_ConvertToString(PyCursesWindowObject *win, PyObject *obj,
}
static int
color_converter(PyObject *arg, void *ptr)
color_allow_default_converter(PyObject *arg, void *ptr)
{
long color_number;
int overflow;
@ -423,19 +409,31 @@ color_converter(PyObject *arg, void *ptr)
if (color_number == -1 && PyErr_Occurred())
return 0;
if (overflow > 0 || color_number > COLORS) {
if (overflow > 0 || color_number >= COLORS) {
PyErr_Format(PyExc_ValueError,
"Color number is greater than COLORS (%d).",
COLORS);
"Color number is greater than COLORS-1 (%d).",
COLORS - 1);
return 0;
}
else if (overflow < 0 || color_number < 0) {
color_number = -1;
}
*(int *)ptr = (int)color_number;
return 1;
}
static int
color_converter(PyObject *arg, void *ptr)
{
if (!color_allow_default_converter(arg, ptr)) {
return 0;
}
if (*(int *)ptr < 0) {
PyErr_SetString(PyExc_ValueError,
"Color number is less than 0.");
return 0;
}
*(int *)ptr = (int)color_number;
return 1;
}
@ -446,6 +444,13 @@ class color_converter(CConverter):
[python start generated code]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=4260d2b6e66b3709]*/
/*[python input]
class color_allow_default_converter(CConverter):
type = 'int'
converter = 'color_allow_default_converter'
[python start generated code]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=975602bc058a872d]*/
static int
pair_converter(PyObject *arg, void *ptr)
{
@ -456,15 +461,24 @@ pair_converter(PyObject *arg, void *ptr)
if (pair_number == -1 && PyErr_Occurred())
return 0;
if (overflow > 0 || pair_number > COLOR_PAIRS - 1) {
#if _NCURSES_EXTENDED_COLOR_FUNCS
if (overflow > 0 || pair_number > INT_MAX) {
PyErr_Format(PyExc_ValueError,
"Color pair is greater than maximum (%d).",
INT_MAX);
return 0;
}
#else
if (overflow > 0 || pair_number >= COLOR_PAIRS) {
PyErr_Format(PyExc_ValueError,
"Color pair is greater than COLOR_PAIRS-1 (%d).",
COLOR_PAIRS - 1);
return 0;
}
else if (overflow < 0 || pair_number < 1) {
#endif
else if (overflow < 0 || pair_number < 0) {
PyErr_SetString(PyExc_ValueError,
"Color pair is less than 1.");
"Color pair is less than 0.");
return 0;
}
@ -742,7 +756,7 @@ _curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1,
if (type == 2) {
funcname = "add_wch";
wstr[1] = L'\0';
setcchar(&wcval, wstr, attr, attr_to_color_pair(attr), NULL);
setcchar(&wcval, wstr, attr, PAIR_NUMBER(attr), NULL);
if (coordinates_group)
rtn = mvwadd_wch(self->win,y,x, &wcval);
else {
@ -2710,7 +2724,7 @@ NoArgOrFlagNoReturnFunctionBody(cbreak, flag)
_curses.color_content
color_number: color
The number of the color (0 - COLORS).
The number of the color (0 - (COLORS-1)).
/
Return the red, green, and blue (RGB) components of the specified color.
@ -2721,7 +2735,7 @@ which will be between 0 (no component) and 1000 (maximum amount of component).
static PyObject *
_curses_color_content_impl(PyObject *module, int color_number)
/*[clinic end generated code: output=17b466df7054e0de input=c10ef58f694b13ee]*/
/*[clinic end generated code: output=17b466df7054e0de input=03b5ed0472662aea]*/
{
_NCURSES_COLOR_VAL_TYPE r,g,b;
@ -2740,8 +2754,8 @@ _curses_color_content_impl(PyObject *module, int color_number)
/*[clinic input]
_curses.color_pair
color_number: color
The number of the color (0 - COLORS).
pair_number: int
The number of the color pair.
/
Return the attribute value for displaying text in the specified color.
@ -2751,13 +2765,13 @@ other A_* attributes. pair_number() is the counterpart to this function.
[clinic start generated code]*/
static PyObject *
_curses_color_pair_impl(PyObject *module, int color_number)
/*[clinic end generated code: output=3fd752e8e24c93fb input=b049033819ab4ef5]*/
_curses_color_pair_impl(PyObject *module, int pair_number)
/*[clinic end generated code: output=60718abb10ce9feb input=6034e9146f343802]*/
{
PyCursesInitialised;
PyCursesInitialisedColor;
return PyLong_FromLong(color_pair_to_attr(color_number));
return PyLong_FromLong(COLOR_PAIR(pair_number));
}
/*[clinic input]
@ -3152,7 +3166,7 @@ _curses_has_key_impl(PyObject *module, int key)
_curses.init_color
color_number: color
The number of the color to be changed (0 - COLORS).
The number of the color to be changed (0 - (COLORS-1)).
r: component
Red component (0 - 1000).
g: component
@ -3165,13 +3179,13 @@ Change the definition of a color.
When init_color() is used, all occurrences of that color on the screen
immediately change to the new definition. This function is a no-op on
most terminals; it is active only if can_change_color() returns 1.
most terminals; it is active only if can_change_color() returns true.
[clinic start generated code]*/
static PyObject *
_curses_init_color_impl(PyObject *module, int color_number, short r, short g,
short b)
/*[clinic end generated code: output=d7ed71b2d818cdf2 input=8a2fe94ca9204aa5]*/
/*[clinic end generated code: output=d7ed71b2d818cdf2 input=ae2b8bea0f152c80]*/
{
PyCursesInitialised;
PyCursesInitialisedColor;
@ -3184,10 +3198,10 @@ _curses.init_pair
pair_number: pair
The number of the color-pair to be changed (1 - (COLOR_PAIRS-1)).
fg: color
Foreground color number (0 - COLORS).
bg: color
Background color number (0 - COLORS).
fg: color_allow_default
Foreground color number (-1 - (COLORS-1)).
bg: color_allow_default
Background color number (-1 - (COLORS-1)).
/
Change the definition of a color-pair.
@ -3198,7 +3212,7 @@ all occurrences of that color-pair are changed to the new definition.
static PyObject *
_curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg)
/*[clinic end generated code: output=a0bba03d2bbc3ee6 input=b865583a18061c1f]*/
/*[clinic end generated code: output=a0bba03d2bbc3ee6 input=54b421b44c12c389]*/
{
PyCursesInitialised;
PyCursesInitialisedColor;
@ -3821,7 +3835,7 @@ NoArgNoReturnFunctionBody(noraw)
_curses.pair_content
pair_number: pair
The number of the color pair (1 - (COLOR_PAIRS-1)).
The number of the color pair (0 - (COLOR_PAIRS-1)).
/
Return a tuple (fg, bg) containing the colors for the requested color pair.
@ -3829,7 +3843,7 @@ Return a tuple (fg, bg) containing the colors for the requested color pair.
static PyObject *
_curses_pair_content_impl(PyObject *module, int pair_number)
/*[clinic end generated code: output=4a726dd0e6885f3f input=b42eacf8a4103852]*/
/*[clinic end generated code: output=4a726dd0e6885f3f input=03970f840fc7b739]*/
{
_NCURSES_COLOR_VAL_TYPE f, b;
@ -3838,7 +3852,7 @@ _curses_pair_content_impl(PyObject *module, int pair_number)
if (_CURSES_PAIR_NUMBER_FUNC(pair_number, &f, &b)==ERR) {
PyErr_SetString(PyCursesError,
"Argument 1 was out of range. (1..COLOR_PAIRS-1)");
"Argument 1 was out of range. (0..COLOR_PAIRS-1)");
return NULL;
}
@ -3863,7 +3877,7 @@ _curses_pair_number_impl(PyObject *module, int attr)
PyCursesInitialised;
PyCursesInitialisedColor;
return PyLong_FromLong(attr_to_color_pair(attr));
return PyLong_FromLong(PAIR_NUMBER(attr));
}
/*[clinic input]

View File

@ -1958,7 +1958,7 @@ PyDoc_STRVAR(_curses_color_content__doc__,
"Return the red, green, and blue (RGB) components of the specified color.\n"
"\n"
" color_number\n"
" The number of the color (0 - COLORS).\n"
" The number of the color (0 - (COLORS-1)).\n"
"\n"
"A 3-tuple is returned, containing the R, G, B values for the given color,\n"
"which will be between 0 (no component) and 1000 (maximum amount of component).");
@ -1985,13 +1985,13 @@ exit:
}
PyDoc_STRVAR(_curses_color_pair__doc__,
"color_pair($module, color_number, /)\n"
"color_pair($module, pair_number, /)\n"
"--\n"
"\n"
"Return the attribute value for displaying text in the specified color.\n"
"\n"
" color_number\n"
" The number of the color (0 - COLORS).\n"
" pair_number\n"
" The number of the color pair.\n"
"\n"
"This attribute value can be combined with A_STANDOUT, A_REVERSE, and the\n"
"other A_* attributes. pair_number() is the counterpart to this function.");
@ -2000,18 +2000,19 @@ PyDoc_STRVAR(_curses_color_pair__doc__,
{"color_pair", (PyCFunction)_curses_color_pair, METH_O, _curses_color_pair__doc__},
static PyObject *
_curses_color_pair_impl(PyObject *module, int color_number);
_curses_color_pair_impl(PyObject *module, int pair_number);
static PyObject *
_curses_color_pair(PyObject *module, PyObject *arg)
{
PyObject *return_value = NULL;
int color_number;
int pair_number;
if (!color_converter(arg, &color_number)) {
pair_number = _PyLong_AsInt(arg);
if (pair_number == -1 && PyErr_Occurred()) {
goto exit;
}
return_value = _curses_color_pair_impl(module, color_number);
return_value = _curses_color_pair_impl(module, pair_number);
exit:
return return_value;
@ -2542,7 +2543,7 @@ PyDoc_STRVAR(_curses_init_color__doc__,
"Change the definition of a color.\n"
"\n"
" color_number\n"
" The number of the color to be changed (0 - COLORS).\n"
" The number of the color to be changed (0 - (COLORS-1)).\n"
" r\n"
" Red component (0 - 1000).\n"
" g\n"
@ -2552,7 +2553,7 @@ PyDoc_STRVAR(_curses_init_color__doc__,
"\n"
"When init_color() is used, all occurrences of that color on the screen\n"
"immediately change to the new definition. This function is a no-op on\n"
"most terminals; it is active only if can_change_color() returns 1.");
"most terminals; it is active only if can_change_color() returns true.");
#define _CURSES_INIT_COLOR_METHODDEF \
{"init_color", (PyCFunction)(void(*)(void))_curses_init_color, METH_FASTCALL, _curses_init_color__doc__},
@ -2600,9 +2601,9 @@ PyDoc_STRVAR(_curses_init_pair__doc__,
" pair_number\n"
" The number of the color-pair to be changed (1 - (COLOR_PAIRS-1)).\n"
" fg\n"
" Foreground color number (0 - COLORS).\n"
" Foreground color number (-1 - (COLORS-1)).\n"
" bg\n"
" Background color number (0 - COLORS).\n"
" Background color number (-1 - (COLORS-1)).\n"
"\n"
"If the color-pair was previously initialized, the screen is refreshed and\n"
"all occurrences of that color-pair are changed to the new definition.");
@ -2627,10 +2628,10 @@ _curses_init_pair(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
if (!pair_converter(args[0], &pair_number)) {
goto exit;
}
if (!color_converter(args[1], &fg)) {
if (!color_allow_default_converter(args[1], &fg)) {
goto exit;
}
if (!color_converter(args[2], &bg)) {
if (!color_allow_default_converter(args[2], &bg)) {
goto exit;
}
return_value = _curses_init_pair_impl(module, pair_number, fg, bg);
@ -3403,7 +3404,7 @@ PyDoc_STRVAR(_curses_pair_content__doc__,
"Return a tuple (fg, bg) containing the colors for the requested color pair.\n"
"\n"
" pair_number\n"
" The number of the color pair (1 - (COLOR_PAIRS-1)).");
" The number of the color pair (0 - (COLOR_PAIRS-1)).");
#define _CURSES_PAIR_CONTENT_METHODDEF \
{"pair_content", (PyCFunction)_curses_pair_content, METH_O, _curses_pair_content__doc__},
@ -4288,4 +4289,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored
#ifndef _CURSES_USE_DEFAULT_COLORS_METHODDEF
#define _CURSES_USE_DEFAULT_COLORS_METHODDEF
#endif /* !defined(_CURSES_USE_DEFAULT_COLORS_METHODDEF) */
/*[clinic end generated code: output=92bad2172fef9747 input=a9049054013a1b77]*/
/*[clinic end generated code: output=ae6559aa61200289 input=a9049054013a1b77]*/