Skip to content

Commit 1470edd

Browse files
bpo-42681: Fix range checks for color and pair numbers in curses (GH-23874)
1 parent 7c83eaa commit 1470edd

File tree

5 files changed

+192
-93
lines changed

5 files changed

+192
-93
lines changed

Doc/library/curses.rst

+8-6
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,15 @@ The module :mod:`curses` defines the following functions:
112112
.. function:: color_content(color_number)
113113

114114
Return the intensity of the red, green, and blue (RGB) components in the color
115-
*color_number*, which must be between ``0`` and :const:`COLORS`. Return a 3-tuple,
115+
*color_number*, which must be between ``0`` and ``COLORS - 1``. Return a 3-tuple,
116116
containing the R,G,B values for the given color, which will be between
117117
``0`` (no component) and ``1000`` (maximum amount of component).
118118

119119

120-
.. function:: color_pair(color_number)
120+
.. function:: color_pair(pair_number)
121121

122-
Return the attribute value for displaying text in the specified color. This
122+
Return the attribute value for displaying text in the specified color pair.
123+
Only the first 256 color pairs are supported. This
123124
attribute value can be combined with :const:`A_STANDOUT`, :const:`A_REVERSE`,
124125
and the other :const:`A_\*` attributes. :func:`pair_number` is the counterpart
125126
to this function.
@@ -287,7 +288,7 @@ The module :mod:`curses` defines the following functions:
287288
Change the definition of a color, taking the number of the color to be changed
288289
followed by three RGB values (for the amounts of red, green, and blue
289290
components). The value of *color_number* must be between ``0`` and
290-
:const:`COLORS`. Each of *r*, *g*, *b*, must be a value between ``0`` and
291+
`COLORS - 1`. Each of *r*, *g*, *b*, must be a value between ``0`` and
291292
``1000``. When :func:`init_color` is used, all occurrences of that color on the
292293
screen immediately change to the new definition. This function is a no-op on
293294
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:
300301
color number. The value of *pair_number* must be between ``1`` and
301302
``COLOR_PAIRS - 1`` (the ``0`` color pair is wired to white on black and cannot
302303
be changed). The value of *fg* and *bg* arguments must be between ``0`` and
303-
:const:`COLORS`. If the color-pair was previously initialized, the screen is
304+
``COLORS - 1``, or, after calling :func:`use_default_colors`, ``-1``.
305+
If the color-pair was previously initialized, the screen is
304306
refreshed and all occurrences of that color-pair are changed to the new
305307
definition.
306308

@@ -450,7 +452,7 @@ The module :mod:`curses` defines the following functions:
450452
.. function:: pair_content(pair_number)
451453

452454
Return a tuple ``(fg, bg)`` containing the colors for the requested color pair.
453-
The value of *pair_number* must be between ``1`` and ``COLOR_PAIRS - 1``.
455+
The value of *pair_number* must be between ``0`` and ``COLOR_PAIRS - 1``.
454456

455457

456458
.. function:: pair_number(attr)

Lib/test/test_curses.py

+107-26
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
# This script doesn't actually display anything very coherent. but it
55
# does call (nearly) every method and function.
66
#
7-
# Functions not tested: {def,reset}_{shell,prog}_mode, getch(), getstr(),
8-
# init_color()
7+
# Functions not tested: {def,reset}_{shell,prog}_mode, getch(), getstr()
98
# Only called, not tested: getmouse(), ungetmouse()
109
#
1110

1211
import os
1312
import string
1413
import sys
1514
import tempfile
15+
import functools
1616
import unittest
1717

1818
from test.support import requires, verbose, SaveSignals
@@ -37,6 +37,15 @@ def requires_curses_func(name):
3737
return unittest.skipUnless(hasattr(curses, name),
3838
'requires curses.%s' % name)
3939

40+
def requires_colors(test):
41+
@functools.wraps(test)
42+
def wrapped(self, *args, **kwargs):
43+
if not curses.has_colors():
44+
self.skipTest('requires colors support')
45+
curses.start_color()
46+
test(self, *args, **kwargs)
47+
return wrapped
48+
4049
term = os.environ.get('TERM')
4150

4251
# If newterm was supported we could use it instead of initscr and not exit
@@ -48,6 +57,8 @@ class TestCurses(unittest.TestCase):
4857

4958
@classmethod
5059
def setUpClass(cls):
60+
if verbose:
61+
print(f'TERM={term}', file=sys.stderr, flush=True)
5162
# testing setupterm() inside initscr/endwin
5263
# causes terminal breakage
5364
stdout_fd = sys.__stdout__.fileno()
@@ -306,31 +317,101 @@ def test_module_funcs(self):
306317
curses.use_env(1)
307318

308319
# Functions only available on a few platforms
309-
def test_colors_funcs(self):
310-
if not curses.has_colors():
311-
self.skipTest('requires colors support')
312-
curses.start_color()
313-
curses.init_pair(2, 1,1)
314-
curses.color_content(1)
315-
curses.color_pair(2)
320+
321+
def bad_colors(self):
322+
return (-1, curses.COLORS, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64)
323+
324+
def bad_colors2(self):
325+
return (curses.COLORS, 2**31, 2**63, 2**64)
326+
327+
def bad_pairs(self):
328+
return (-1, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64)
329+
330+
@requires_colors
331+
def test_color_content(self):
332+
self.assertEqual(curses.color_content(curses.COLOR_BLACK), (0, 0, 0))
333+
curses.color_content(0)
334+
curses.color_content(curses.COLORS - 1)
335+
336+
for color in self.bad_colors():
337+
self.assertRaises(ValueError, curses.color_content, color)
338+
339+
@requires_colors
340+
def test_init_color(self):
341+
if not curses.can_change_color:
342+
self.skipTest('cannot change color')
343+
344+
old = curses.color_content(0)
345+
try:
346+
curses.init_color(0, *old)
347+
except curses.error:
348+
self.skipTest('cannot change color (init_color() failed)')
349+
self.addCleanup(curses.init_color, 0, *old)
350+
curses.init_color(0, 0, 0, 0)
351+
self.assertEqual(curses.color_content(0), (0, 0, 0))
352+
curses.init_color(0, 1000, 1000, 1000)
353+
self.assertEqual(curses.color_content(0), (1000, 1000, 1000))
354+
355+
old = curses.color_content(curses.COLORS - 1)
356+
curses.init_color(curses.COLORS - 1, *old)
357+
self.addCleanup(curses.init_color, curses.COLORS - 1, *old)
358+
curses.init_color(curses.COLORS - 1, 0, 500, 1000)
359+
self.assertEqual(curses.color_content(curses.COLORS - 1), (0, 500, 1000))
360+
361+
for color in self.bad_colors():
362+
self.assertRaises(ValueError, curses.init_color, color, 0, 0, 0)
363+
for comp in (-1, 1001):
364+
self.assertRaises(ValueError, curses.init_color, 0, comp, 0, 0)
365+
self.assertRaises(ValueError, curses.init_color, 0, 0, comp, 0)
366+
self.assertRaises(ValueError, curses.init_color, 0, 0, 0, comp)
367+
368+
@requires_colors
369+
def test_pair_content(self):
370+
if not hasattr(curses, 'use_default_colors'):
371+
self.assertEqual(curses.pair_content(0),
372+
(curses.COLOR_WHITE, curses.COLOR_BLACK))
373+
curses.pair_content(0)
316374
curses.pair_content(curses.COLOR_PAIRS - 1)
317-
curses.pair_number(0)
318-
319-
if hasattr(curses, 'use_default_colors'):
320-
curses.use_default_colors()
321-
322-
self.assertRaises(ValueError, curses.color_content, -1)
323-
self.assertRaises(ValueError, curses.color_content, curses.COLORS + 1)
324-
self.assertRaises(ValueError, curses.color_content, -2**31 - 1)
325-
self.assertRaises(ValueError, curses.color_content, 2**31)
326-
self.assertRaises(ValueError, curses.color_content, -2**63 - 1)
327-
self.assertRaises(ValueError, curses.color_content, 2**63 - 1)
328-
self.assertRaises(ValueError, curses.pair_content, -1)
329-
self.assertRaises(ValueError, curses.pair_content, curses.COLOR_PAIRS)
330-
self.assertRaises(ValueError, curses.pair_content, -2**31 - 1)
331-
self.assertRaises(ValueError, curses.pair_content, 2**31)
332-
self.assertRaises(ValueError, curses.pair_content, -2**63 - 1)
333-
self.assertRaises(ValueError, curses.pair_content, 2**63 - 1)
375+
376+
for pair in self.bad_pairs():
377+
self.assertRaises(ValueError, curses.pair_content, pair)
378+
379+
@requires_colors
380+
def test_init_pair(self):
381+
old = curses.pair_content(1)
382+
curses.init_pair(1, *old)
383+
self.addCleanup(curses.init_pair, 1, *old)
384+
385+
curses.init_pair(1, 0, 0)
386+
self.assertEqual(curses.pair_content(1), (0, 0))
387+
curses.init_pair(1, curses.COLORS - 1, curses.COLORS - 1)
388+
self.assertEqual(curses.pair_content(1),
389+
(curses.COLORS - 1, curses.COLORS - 1))
390+
curses.init_pair(curses.COLOR_PAIRS - 1, 2, 3)
391+
self.assertEqual(curses.pair_content(curses.COLOR_PAIRS - 1), (2, 3))
392+
393+
for pair in self.bad_pairs():
394+
self.assertRaises(ValueError, curses.init_pair, pair, 0, 0)
395+
for color in self.bad_colors2():
396+
self.assertRaises(ValueError, curses.init_pair, 1, color, 0)
397+
self.assertRaises(ValueError, curses.init_pair, 1, 0, color)
398+
399+
@requires_colors
400+
def test_color_attrs(self):
401+
for pair in 0, 1, 255:
402+
attr = curses.color_pair(pair)
403+
self.assertEqual(curses.pair_number(attr), pair, attr)
404+
self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair)
405+
self.assertEqual(curses.color_pair(0), 0)
406+
self.assertEqual(curses.pair_number(0), 0)
407+
408+
@requires_curses_func('use_default_colors')
409+
@requires_colors
410+
def test_use_default_colors(self):
411+
self.assertIn(curses.pair_content(0),
412+
((curses.COLOR_WHITE, curses.COLOR_BLACK), (-1, -1)))
413+
curses.use_default_colors()
414+
self.assertEqual(curses.pair_content(0), (-1, -1))
334415

335416
@requires_curses_func('keyname')
336417
def test_keyname(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed range checks for color and pair numbers in :mod:`curses`.

0 commit comments

Comments
 (0)