Skip to content

Commit

Permalink
bpo-42681: Fix range checks for color and pair numbers in curses (GH-…
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka authored Jan 3, 2021
1 parent 7c83eaa commit 1470edd
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 93 deletions.
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 @@ -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``.
Expand All @@ -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.

Expand Down Expand Up @@ -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)
Expand Down
133 changes: 107 additions & 26 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, verbose, SaveSignals
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -306,31 +317,101 @@ 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)

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()

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)

for pair in self.bad_pairs():
self.assertRaises(ValueError, curses.pair_content, pair)

@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):
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`.
Loading

0 comments on commit 1470edd

Please sign in to comment.