diff --git a/Doc/library/base64.rst b/Doc/library/base64.rst index 4876117f6403b2..3e7884debd5948 100644 --- a/Doc/library/base64.rst +++ b/Doc/library/base64.rst @@ -84,15 +84,20 @@ POST request. A :exc:`binascii.Error` exception is raised if *s* is incorrectly padded. - If *validate* is ``False`` (the default), characters that are neither + If *validate* is false (the default), characters that are neither in the normal base-64 alphabet nor the alternative alphabet are - discarded prior to the padding check. If *validate* is ``True``, - these non-alphabet characters in the input result in a - :exc:`binascii.Error`. + discarded prior to the padding check, but the ``+`` and ``/`` characters + keep their meaning if they are not in *altchars* (they will be discarded + in future Python versions). + If *validate* is true, these non-alphabet characters in the input + result in a :exc:`binascii.Error`. For more information about the strict base64 check, see :func:`binascii.a2b_base64` - May assert or raise a :exc:`ValueError` if the length of *altchars* is not 2. + .. deprecated:: next + Accepting the ``+`` and ``/`` characters with an alternative alphabet + is now deprecated. + .. function:: standard_b64encode(s) @@ -123,6 +128,9 @@ POST request. ``/`` in the standard Base64 alphabet, and return the decoded :class:`bytes`. + .. deprecated:: next + Accepting the ``+`` and ``/`` characters is now deprecated. + .. function:: b32encode(s) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 1dd66065b0f93d..1d4961d7293631 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1149,6 +1149,15 @@ Deprecated New deprecations ---------------- +* :mod:`base64`: + + * Accepting the ``+`` and ``/`` characters with an alternative alphabet in + :func:`~base64.b64decode` and :func:`~base64.urlsafe_b64decode` is now + deprecated. + In future Python versions they will be errors in the strict mode and + discarded in the non-strict mode. + (Contributed by Serhiy Storchaka in :gh:`125346`.) + * CLI: * Deprecate :option:`-b` and :option:`!-bb` command-line options diff --git a/Lib/base64.py b/Lib/base64.py index e62ae6aff580fa..6e0da16b23ce99 100644 --- a/Lib/base64.py +++ b/Lib/base64.py @@ -72,20 +72,39 @@ def b64decode(s, altchars=None, validate=False): The result is returned as a bytes object. A binascii.Error is raised if s is incorrectly padded. - If validate is False (the default), characters that are neither in the + If validate is false (the default), characters that are neither in the normal base-64 alphabet nor the alternative alphabet are discarded prior - to the padding check. If validate is True, these non-alphabet characters + to the padding check. If validate is true, these non-alphabet characters in the input result in a binascii.Error. For more information about the strict base64 check, see: https://docs.python.org/3.11/library/binascii.html#binascii.a2b_base64 """ s = _bytes_from_decode_data(s) + badchar = None if altchars is not None: altchars = _bytes_from_decode_data(altchars) - assert len(altchars) == 2, repr(altchars) + if len(altchars) != 2: + raise ValueError(f'invalid altchars: {altchars!r}') + for b in b'+/': + if b not in altchars and b in s: + badchar = b + break s = s.translate(bytes.maketrans(altchars, b'+/')) - return binascii.a2b_base64(s, strict_mode=validate) + result = binascii.a2b_base64(s, strict_mode=validate) + if badchar is not None: + import warnings + if validate: + warnings.warn(f'invalid character {chr(badchar)!a} in Base64 data ' + f'with altchars={altchars!r} and validate=True ' + f'will be an error in future Python versions', + DeprecationWarning, stacklevel=2) + else: + warnings.warn(f'invalid character {chr(badchar)!a} in Base64 data ' + f'with altchars={altchars!r} and validate=False ' + f'will be discarded in future Python versions', + FutureWarning, stacklevel=2) + return result def standard_b64encode(s): @@ -130,8 +149,19 @@ def urlsafe_b64decode(s): The alphabet uses '-' instead of '+' and '_' instead of '/'. """ s = _bytes_from_decode_data(s) + badchar = None + for b in b'+/': + if b in s: + badchar = b + break s = s.translate(_urlsafe_decode_translation) - return b64decode(s) + result = binascii.a2b_base64(s, strict_mode=False) + if badchar is not None: + import warnings + warnings.warn(f'invalid character {chr(badchar)!a} in URL-safe Base64 data ' + f'will be discarded in future Python versions', + FutureWarning, stacklevel=2) + return result diff --git a/Lib/inspect.py b/Lib/inspect.py index 07c4e28f0d9952..0dba3c6628c6e5 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -348,6 +348,7 @@ def isgenerator(object): gi_frame frame object or possibly None once the generator has been exhausted gi_running set to 1 when generator is executing, 0 otherwise + gi_suspended set to 1 when the generator is suspended at a yield point, 0 otherwise gi_yieldfrom object being iterated by yield from or None __iter__() defined to support iteration over container diff --git a/Lib/test/test_base64.py b/Lib/test/test_base64.py index 120c5824a42a40..d02992903f15a7 100644 --- a/Lib/test/test_base64.py +++ b/Lib/test/test_base64.py @@ -292,6 +292,11 @@ def test_b64decode_altchars(self): eq(base64.b64decode(data, altchars=altchars_str), res) eq(base64.b64decode(data_str, altchars=altchars_str), res) + self.assertRaises(ValueError, base64.b64decode, b'', altchars=b'+') + self.assertRaises(ValueError, base64.b64decode, b'', altchars=b'+/-') + self.assertRaises(ValueError, base64.b64decode, '', altchars='+') + self.assertRaises(ValueError, base64.b64decode, '', altchars='+/-') + def test_b64decode_padding_error(self): self.assertRaises(binascii.Error, base64.b64decode, b'abc') self.assertRaises(binascii.Error, base64.b64decode, 'abc') @@ -323,13 +328,25 @@ def test_b64decode_invalid_chars(self): with self.assertRaises(binascii.Error): base64.b64decode(bstr.decode('ascii'), validate=True) - # Normal alphabet characters not discarded when alternative given - res = b'\xfb\xef\xff' - self.assertEqual(base64.b64decode(b'++//', validate=True), res) - self.assertEqual(base64.b64decode(b'++//', '-_', validate=True), res) - self.assertEqual(base64.b64decode(b'--__', '-_', validate=True), res) - self.assertEqual(base64.urlsafe_b64decode(b'++//'), res) - self.assertEqual(base64.urlsafe_b64decode(b'--__'), res) + # Normal alphabet characters will be discarded when alternative given + with self.assertWarns(FutureWarning): + self.assertEqual(base64.b64decode(b'++++', altchars=b'-_'), + b'\xfb\xef\xbe') + with self.assertWarns(FutureWarning): + self.assertEqual(base64.b64decode(b'////', altchars=b'-_'), + b'\xff\xff\xff') + with self.assertWarns(DeprecationWarning): + self.assertEqual(base64.b64decode(b'++++', altchars=b'-_', validate=True), + b'\xfb\xef\xbe') + with self.assertWarns(DeprecationWarning): + self.assertEqual(base64.b64decode(b'////', altchars=b'-_', validate=True), + b'\xff\xff\xff') + with self.assertWarns(FutureWarning): + self.assertEqual(base64.urlsafe_b64decode(b'++++'), b'\xfb\xef\xbe') + with self.assertWarns(FutureWarning): + self.assertEqual(base64.urlsafe_b64decode(b'////'), b'\xff\xff\xff') + with self.assertRaises(binascii.Error): + base64.b64decode(b'+/!', altchars=b'-_') def _altchars_strategy(): """Generate 'altchars' for base64 encoding.""" diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 4595e7e5d3edc1..b0d2348c0e1cba 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -2294,8 +2294,8 @@ def foo(): return gen self.assertIs(wrapper.__name__, gen.__name__) # Test AttributeErrors - for name in {'gi_running', 'gi_frame', 'gi_code', 'gi_yieldfrom', - 'cr_running', 'cr_frame', 'cr_code', 'cr_await'}: + for name in {'gi_running', 'gi_frame', 'gi_code', 'gi_yieldfrom', 'gi_suspended', + 'cr_running', 'cr_frame', 'cr_code', 'cr_await', 'cr_suspended'}: with self.assertRaises(AttributeError): getattr(wrapper, name) @@ -2304,14 +2304,17 @@ def foo(): return gen gen.gi_frame = object() gen.gi_code = object() gen.gi_yieldfrom = object() + gen.gi_suspended = object() self.assertIs(wrapper.gi_running, gen.gi_running) self.assertIs(wrapper.gi_frame, gen.gi_frame) self.assertIs(wrapper.gi_code, gen.gi_code) self.assertIs(wrapper.gi_yieldfrom, gen.gi_yieldfrom) + self.assertIs(wrapper.gi_suspended, gen.gi_suspended) self.assertIs(wrapper.cr_running, gen.gi_running) self.assertIs(wrapper.cr_frame, gen.gi_frame) self.assertIs(wrapper.cr_code, gen.gi_code) self.assertIs(wrapper.cr_await, gen.gi_yieldfrom) + self.assertIs(wrapper.cr_suspended, gen.gi_suspended) wrapper.close() gen.close.assert_called_once_with() @@ -2430,7 +2433,7 @@ def foo(): return gen self.assertIs(wrapper.__await__(), gen) for name in ('__name__', '__qualname__', 'gi_code', - 'gi_running', 'gi_frame'): + 'gi_running', 'gi_frame', 'gi_suspended'): self.assertIs(getattr(foo(), name), getattr(gen, name)) self.assertIs(foo().cr_code, gen.gi_code) @@ -2493,8 +2496,8 @@ def coro(): self.assertEqual(repr(wrapper), str(wrapper)) self.assertTrue(set(dir(wrapper)).issuperset({ '__await__', '__iter__', '__next__', 'cr_code', 'cr_running', - 'cr_frame', 'gi_code', 'gi_frame', 'gi_running', 'send', - 'close', 'throw'})) + 'cr_frame', 'cr_suspended', 'gi_code', 'gi_frame', 'gi_running', + 'gi_suspended', 'send', 'close', 'throw'})) class FunctionTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst b/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst new file mode 100644 index 00000000000000..187a6ebbe79b26 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-06-12-03-29.gh-issue-125346.7Gfpgw.rst @@ -0,0 +1,5 @@ +Accepting ``+`` and ``/`` characters with an alternative alphabet in +:func:`base64.b64decode` and :func:`base64.urlsafe_b64decode` is now +deprecated. +In future Python versions they will be errors in the strict mode and +discarded in the non-strict mode.