[3.9] bpo-42681: Fix range checks for color and pair numbers in curses (GH-23874). (GH-24077)

(cherry picked from commit 1470edd613)
This commit is contained in:
Serhiy Storchaka 2021-01-04 00:55:23 +02:00 committed by GitHub
parent 0303008ebc
commit b0ee2b492d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 60 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.
@ -278,7 +279,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``.
@ -291,7 +292,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.
@ -441,7 +443,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, import_module, verbose, SaveSignals
@ -36,7 +36,17 @@ 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')
SHORT_MAX = 0x7fff
# If newterm was supported we could use it instead of initscr and not exit
@unittest.skipIf(not term or term == 'unknown',
@ -47,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()
@ -304,18 +316,111 @@ 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)
curses.pair_content(min(curses.COLOR_PAIRS - 1, 0x7fff))
curses.pair_number(0)
if hasattr(curses, 'use_default_colors'):
curses.use_default_colors()
def bad_colors(self):
return (-2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64)
def bad_pairs(self):
return (-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(min(curses.COLORS - 1, SHORT_MAX))
for color in self.bad_colors():
self.assertRaises(OverflowError, curses.color_content, color)
if curses.COLORS <= SHORT_MAX:
self.assertRaises(curses.error, curses.color_content, curses.COLORS)
self.assertRaises(curses.error, curses.color_content, -1)
@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))
maxcolor = min(curses.COLORS - 1, SHORT_MAX)
old = curses.color_content(maxcolor)
curses.init_color(maxcolor, *old)
self.addCleanup(curses.init_color, maxcolor, *old)
curses.init_color(maxcolor, 0, 500, 1000)
self.assertEqual(curses.color_content(maxcolor), (0, 500, 1000))
for color in self.bad_colors():
self.assertRaises(OverflowError, curses.init_color, color, 0, 0, 0)
if curses.COLORS <= SHORT_MAX:
self.assertRaises(curses.error, curses.init_color, curses.COLORS, 0, 0, 0)
self.assertRaises(curses.error, curses.init_color, -1, 0, 0, 0)
for comp in (-1, 1001):
self.assertRaises(curses.error, curses.init_color, 0, comp, 0, 0)
self.assertRaises(curses.error, curses.init_color, 0, 0, comp, 0)
self.assertRaises(curses.error, 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(min(curses.COLOR_PAIRS - 1, SHORT_MAX))
for pair in self.bad_pairs():
self.assertRaises(OverflowError, curses.pair_content, pair)
self.assertRaises(curses.error, curses.pair_content, -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))
maxcolor = min(curses.COLORS - 1, SHORT_MAX)
curses.init_pair(1, maxcolor, maxcolor)
self.assertEqual(curses.pair_content(1), (maxcolor, maxcolor))
maxpair = min(curses.COLOR_PAIRS - 1, SHORT_MAX)
curses.init_pair(maxpair, 2, 3)
self.assertEqual(curses.pair_content(maxpair), (2, 3))
for pair in self.bad_pairs():
self.assertRaises(OverflowError, curses.init_pair, pair, 0, 0)
self.assertRaises(curses.error, curses.init_pair, -1, 0, 0)
for color in self.bad_colors():
self.assertRaises(OverflowError, curses.init_pair, 1, color, 0)
self.assertRaises(OverflowError, curses.init_pair, 1, 0, color)
if curses.COLORS <= SHORT_MAX:
self.assertRaises(curses.error, curses.init_pair, 1, curses.COLORS, 0)
self.assertRaises(curses.error, curses.init_pair, 1, 0, curses.COLORS)
@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

@ -176,18 +176,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
@ -618,7 +606,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 {
@ -2586,7 +2574,7 @@ NoArgOrFlagNoReturnFunctionBody(cbreak, flag)
_curses.color_content
color_number: short
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.
@ -2597,7 +2585,7 @@ which will be between 0 (no component) and 1000 (maximum amount of component).
static PyObject *
_curses_color_content_impl(PyObject *module, short color_number)
/*[clinic end generated code: output=cb15cf3120d4bfc1 input=5555abb1c11e11b7]*/
/*[clinic end generated code: output=cb15cf3120d4bfc1 input=630f6737514db6ad]*/
{
short r,g,b;
@ -2616,8 +2604,8 @@ _curses_color_content_impl(PyObject *module, short color_number)
/*[clinic input]
_curses.color_pair
color_number: short
The number of the color (0 - COLORS).
pair_number: short
The number of the color pair.
/
Return the attribute value for displaying text in the specified color.
@ -2627,13 +2615,13 @@ other A_* attributes. pair_number() is the counterpart to this function.
[clinic start generated code]*/
static PyObject *
_curses_color_pair_impl(PyObject *module, short color_number)
/*[clinic end generated code: output=6a84cb6b29ecaf9a input=a9d3eb6f50e4dc12]*/
_curses_color_pair_impl(PyObject *module, short pair_number)
/*[clinic end generated code: output=ce609d238b70dc11 input=8dd0d5da94cb15b5]*/
{
PyCursesInitialised;
PyCursesInitialisedColor;
return PyLong_FromLong(color_pair_to_attr(color_number));
return PyLong_FromLong(COLOR_PAIR(pair_number));
}
/*[clinic input]
@ -3028,7 +3016,7 @@ _curses_has_key_impl(PyObject *module, int key)
_curses.init_color
color_number: short
The number of the color to be changed (0 - COLORS).
The number of the color to be changed (0 - (COLORS-1)).
r: short
Red component (0 - 1000).
g: short
@ -3041,13 +3029,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, short color_number, short r,
short g, short b)
/*[clinic end generated code: output=280236f5efe9776a input=f3a05bd38f619175]*/
/*[clinic end generated code: output=280236f5efe9776a input=128601b5dc76d548]*/
{
PyCursesInitialised;
PyCursesInitialisedColor;
@ -3061,9 +3049,9 @@ _curses.init_pair
pair_number: short
The number of the color-pair to be changed (1 - (COLOR_PAIRS-1)).
fg: short
Foreground color number (0 - COLORS).
Foreground color number (-1 - (COLORS-1)).
bg: short
Background color number (0 - COLORS).
Background color number (-1 - (COLORS-1)).
/
Change the definition of a color-pair.
@ -3075,7 +3063,7 @@ all occurrences of that color-pair are changed to the new definition.
static PyObject *
_curses_init_pair_impl(PyObject *module, short pair_number, short fg,
short bg)
/*[clinic end generated code: output=9c2ce39c22f376b6 input=c9f0b11b17a2ac6d]*/
/*[clinic end generated code: output=9c2ce39c22f376b6 input=12c320ec14396ea2]*/
{
PyCursesInitialised;
PyCursesInitialisedColor;
@ -3715,7 +3703,7 @@ _curses_pair_content_impl(PyObject *module, short pair_number)
if (pair_content(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;
}
@ -3740,7 +3728,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

@ -2028,7 +2028,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).");
@ -2076,13 +2076,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.");
@ -2091,13 +2091,13 @@ 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, short color_number);
_curses_color_pair_impl(PyObject *module, short pair_number);
static PyObject *
_curses_color_pair(PyObject *module, PyObject *arg)
{
PyObject *return_value = NULL;
short color_number;
short pair_number;
if (PyFloat_Check(arg)) {
PyErr_SetString(PyExc_TypeError,
@ -2120,10 +2120,10 @@ _curses_color_pair(PyObject *module, PyObject *arg)
goto exit;
}
else {
color_number = (short) ival;
pair_number = (short) ival;
}
}
return_value = _curses_color_pair_impl(module, color_number);
return_value = _curses_color_pair_impl(module, pair_number);
exit:
return return_value;
@ -2699,7 +2699,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"
@ -2709,7 +2709,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__},
@ -2841,9 +2841,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.");
@ -4713,4 +4713,4 @@ _curses_use_default_colors(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=b53652f8acafd817 input=a9049054013a1b77]*/
/*[clinic end generated code: output=5e739120041df368 input=a9049054013a1b77]*/