Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jan 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions Doc/library/curses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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``.
Expand All @@ -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.

Expand Down Expand Up @@ -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)
Expand Down
133 changes: 119 additions & 14 deletions Lib/test/test_curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
# 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()
#

import os
import string
import sys
import tempfile
import functools
import unittest

from test.support import requires, import_module, verbose, SaveSignals
Expand All @@ -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',
Expand All @@ -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()
Expand Down Expand Up @@ -304,18 +316,111 @@ def test_module_funcs(self):
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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed range checks for color and pair numbers in :mod:`curses`.
44 changes: 16 additions & 28 deletions Modules/_cursesmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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;

Expand All @@ -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.
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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]
Expand Down
Loading