diff --git a/Doc/library/binascii.rst b/Doc/library/binascii.rst index 4f2edb7eff8a8f..f6e4154b518b86 100644 --- a/Doc/library/binascii.rst +++ b/Doc/library/binascii.rst @@ -349,9 +349,14 @@ The :mod:`!binascii` module defines the following functions: .. exception:: Incomplete - Exception raised on incomplete data. These are usually not programming errors, + A subclass of :exc:`Error` raised on incomplete data. + These are usually not programming errors, but may be handled by reading a little more data and trying again. + .. versionchanged:: next + Made :exc:`!Incomplete` a subclass of :exc:`!Error`. + It is now raised on incomplete Base16, Base32, and Base64 data. + .. data:: BASE64_ALPHABET diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d1d4b92bcf4e97..7347e84a41ab2b 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -709,6 +709,10 @@ binascii :func:`~binascii.unhexlify`, and :func:`~binascii.a2b_base64`. (Contributed by Serhiy Storchaka in :gh:`144001` and :gh:`146431`.) +* :exc:`~binascii.Incomplete`, which is now a subclass of :exc:`~binascii.Error`, + is now raised for incomplete data instead of :exc:`!Error`. + (Contributed by Serhiy Storchaka in :gh:`148108`.) + calendar -------- diff --git a/Lib/test/test_base64.py b/Lib/test/test_base64.py index 1a4dd56a553f4d..5d0d96a0871398 100644 --- a/Lib/test/test_base64.py +++ b/Lib/test/test_base64.py @@ -330,8 +330,8 @@ def test_b64decode_altchars(self): 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') + self.assertRaises(binascii.Incomplete, base64.b64decode, b'abc') + self.assertRaises(binascii.Incomplete, base64.b64decode, 'abc') def test_b64decode_padded(self): b64decode = base64.b64decode @@ -358,9 +358,9 @@ def check(data, expected, padded=0): check(b'YW=Jj', b'abc') check(b'YWJ=j', b'abc') - with self.assertRaisesRegex(binascii.Error, 'Incorrect padding'): + with self.assertRaisesRegex(binascii.Incomplete, 'Incorrect padding'): urlsafe_b64decode(b'YQ', padded=True) - with self.assertRaisesRegex(binascii.Error, 'Incorrect padding'): + with self.assertRaisesRegex(binascii.Incomplete, 'Incorrect padding'): urlsafe_b64decode(b'YWI', padded=True) def _common_test_ignorechars(self, func): @@ -483,7 +483,7 @@ def test_b64decode_invalid_chars(self): self.assertEqual(str(cm.warning), "invalid character '/' in URL-safe Base64 data " "will be discarded in future Python versions") - with self.assertRaises(binascii.Error): + with self.assertRaises(binascii.Incomplete): base64.b64decode(b'+/!', altchars=b'-_') def _altchars_strategy(): @@ -879,7 +879,7 @@ def test_b16decode(self): # Non-alphabet characters self.assertRaises(binascii.Error, base64.b16decode, '0102AG') # Incorrect "padding" - self.assertRaises(binascii.Error, base64.b16decode, '010') + self.assertRaises(binascii.Incomplete, base64.b16decode, '010') def test_b16decode_ignorechars(self): self._common_test_ignorechars(base64.b16decode) @@ -1226,9 +1226,9 @@ def test_a85decode_errors(self): with self.assertRaises(ValueError, msg=bytes([c])): base64.a85decode(b'<~!!!!' + bytes([c]) + b'~>', adobe=True) - self.assertRaises(ValueError, base64.a85decode, + self.assertRaises(binascii.Incomplete, base64.a85decode, b"malformed", adobe=True) - self.assertRaises(ValueError, base64.a85decode, + self.assertRaises(binascii.Incomplete, base64.a85decode, b"<~still malformed", adobe=True) # With adobe=False (the default), Adobe framing markers are disallowed diff --git a/Lib/test/test_binascii.py b/Lib/test/test_binascii.py index 81cdacb96241e2..4ee85b3ebc7adb 100644 --- a/Lib/test/test_binascii.py +++ b/Lib/test/test_binascii.py @@ -343,12 +343,12 @@ def test_base64errors(self): # Test base64 with invalid padding def assertIncorrectPadding(data, strict_mode=True): data = self.type2test(data) - with self.assertRaisesRegex(binascii.Error, r'(?i)Incorrect padding'): + with self.assertRaisesRegex(binascii.Incomplete, r'(?i)Incorrect padding'): binascii.a2b_base64(data) - with self.assertRaisesRegex(binascii.Error, r'(?i)Incorrect padding'): + with self.assertRaisesRegex(binascii.Incomplete, r'(?i)Incorrect padding'): binascii.a2b_base64(data, strict_mode=False) if strict_mode: - with self.assertRaisesRegex(binascii.Error, r'(?i)Incorrect padding'): + with self.assertRaisesRegex(binascii.Incomplete, r'(?i)Incorrect padding'): binascii.a2b_base64(data, strict_mode=True) assertIncorrectPadding(b'ab') @@ -361,18 +361,23 @@ def assertIncorrectPadding(data, strict_mode=True): assertIncorrectPadding(b'a\nb=', strict_mode=False) # Test base64 with invalid number of valid characters (1 mod 4) - def assertInvalidLength(data, strict_mode=True): + def assertInvalidLength(data, strict_mode=True, incomplete=True): + strict_incomplete = b'=' not in data n_data_chars = len(re.sub(br'[^A-Za-z0-9/+]', br'', data)) data = self.type2test(data) expected_errmsg_re = \ r'(?i)Invalid.+number of data characters.+' + str(n_data_chars) - with self.assertRaisesRegex(binascii.Error, expected_errmsg_re): + with self.assertRaisesRegex(binascii.Incomplete, expected_errmsg_re): binascii.a2b_base64(data) - with self.assertRaisesRegex(binascii.Error, expected_errmsg_re): + with self.assertRaisesRegex(binascii.Incomplete, expected_errmsg_re): binascii.a2b_base64(data, strict_mode=False) if strict_mode: - with self.assertRaisesRegex(binascii.Error, expected_errmsg_re): - binascii.a2b_base64(data, strict_mode=True) + if strict_incomplete: + with self.assertRaisesRegex(binascii.Incomplete, expected_errmsg_re): + binascii.a2b_base64(data, strict_mode=True) + else: + with self.assertRaisesRegex(binascii.Error, expected_errmsg_re): + binascii.a2b_base64(data, strict_mode=True) assertInvalidLength(b'a') assertInvalidLength(b'a=') @@ -495,12 +500,14 @@ def addnoise(line): self.assertEqual(b, b"") def test_ascii85_errors(self): - def _assertRegexTemplate(assert_regex, data, **kwargs): - with self.assertRaisesRegex(binascii.Error, assert_regex): + def _assertRegexTemplate(assert_regex, data, *, + expected_errtype=binascii.Error, **kwargs): + with self.assertRaisesRegex(expected_errtype, assert_regex): binascii.a2b_ascii85(self.type2test(data), **kwargs) def assertMissingDelimiter(data): - _assertRegexTemplate(r"(?i)end with b'~>'", data, adobe=True) + _assertRegexTemplate(r"(?i)end with b'~>'", data, adobe=True, + expected_errtype=binascii.Incomplete) def assertOverflow(data): _assertRegexTemplate(r"(?i)Ascii85 overflow", data) @@ -792,8 +799,9 @@ def _fixPadding(data): p = 8 - len_8 if len_8 else 0 return fixed + b"=" * p - def _assertRegexTemplate(assert_regex, data, good_padding_result=None, **kwargs): - with self.assertRaisesRegex(binascii.Error, assert_regex): + def _assertRegexTemplate(assert_regex, data, good_padding_result=None, *, + expected_errtype=binascii.Error, **kwargs): + with self.assertRaisesRegex(expected_errtype, assert_regex): binascii.a2b_base32(self.type2test(data), **kwargs) if good_padding_result: fixed = self.type2test(_fixPadding(data)) @@ -812,7 +820,8 @@ def assertLeadingPadding(*args, **kwargs): _assertRegexTemplate(r"(?i)Leading padding", *args, **kwargs) def assertIncorrectPadding(*args): - _assertRegexTemplate(r"(?i)Incorrect padding", *args) + _assertRegexTemplate(r"(?i)Incorrect padding", *args, + expected_errtype=binascii.Incomplete) def assertDiscontinuousPadding(*args): _assertRegexTemplate(r"(?i)Discontinuous padding", *args) @@ -820,8 +829,12 @@ def assertDiscontinuousPadding(*args): def assertInvalidLength(data, *args, length=None, **kwargs): if length is None: length = len(data.split(b'=', 1)[0].replace(b' ', b'')) + incomplete = b'=' not in data + expected_errtype = binascii.Incomplete if incomplete else binascii.Error assert_regex = fr"(?i)Invalid.+number of data characters \({length}\)" - _assertRegexTemplate(assert_regex, data, *args, **kwargs) + _assertRegexTemplate(assert_regex, data, *args, + expected_errtype=expected_errtype, + **kwargs) assertNonBase32Data(b"a") assertNonBase32Data(b"AA-") @@ -1098,7 +1111,7 @@ def test_hex(self): t = binascii.b2a_hex(self.type2test(s)) u = binascii.a2b_hex(self.type2test(t)) self.assertEqual(s, u) - self.assertRaises(binascii.Error, binascii.a2b_hex, t[:-1]) + self.assertRaises(binascii.Incomplete, binascii.a2b_hex, t[:-1]) self.assertRaises(binascii.Error, binascii.a2b_hex, t[:-1] + b'q') self.assertRaises(binascii.Error, binascii.a2b_hex, bytes([255, 255])) self.assertRaises(binascii.Error, binascii.a2b_hex, b'0G') diff --git a/Misc/NEWS.d/next/Library/2026-04-04-23-00-33.gh-issue-148108.TAB7NW.rst b/Misc/NEWS.d/next/Library/2026-04-04-23-00-33.gh-issue-148108.TAB7NW.rst new file mode 100644 index 00000000000000..1109bf45c810a3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-04-23-00-33.gh-issue-148108.TAB7NW.rst @@ -0,0 +1,2 @@ +Make :exc:`binascii.Incomplete` a subclass of :exc:`binascii.Error` and use +it for incomplete data errors. diff --git a/Modules/binascii.c b/Modules/binascii.c index 9193137877aef9..ebd2fdf50dfebc 100644 --- a/Modules/binascii.c +++ b/Modules/binascii.c @@ -892,7 +892,7 @@ binascii_a2b_base64_impl(PyObject *module, Py_buffer *data, int strict_mode, state = get_binascii_state(module); if (state) { unsigned char *bin_data_start = PyBytesWriter_GetData(writer); - PyErr_Format(state->Error, + PyErr_Format(ascii_len ? state->Error : state->Incomplete, "Invalid base64-encoded string: " "number of data characters (%zd) cannot be 1 more " "than a multiple of 4", @@ -904,7 +904,7 @@ binascii_a2b_base64_impl(PyObject *module, Py_buffer *data, int strict_mode, if (padded && quad_pos != 0 && quad_pos + pads < 4) { state = get_binascii_state(module); if (state) { - PyErr_SetString(state->Error, "Incorrect padding"); + PyErr_SetString(state->Incomplete, "Incorrect padding"); } goto error_end; } @@ -1060,7 +1060,7 @@ binascii_a2b_ascii85_impl(PyObject *module, Py_buffer *data, int foldspaces, { state = get_binascii_state(module); if (state != NULL) { - PyErr_SetString(state->Error, + PyErr_SetString(state->Incomplete, "Ascii85 encoded byte sequences must end with b'~>'"); } return NULL; @@ -1706,7 +1706,7 @@ binascii_a2b_base32_impl(PyObject *module, Py_buffer *data, int padded, state = get_binascii_state(module); if (state) { unsigned char *bin_data_start = PyBytesWriter_GetData(writer); - PyErr_Format(state->Error, + PyErr_Format(ascii_len ? state->Error : state->Incomplete, "Invalid base32-encoded string: " "number of data characters (%zd) " "cannot be 1, 3, or 6 more than a multiple of 8", @@ -1718,7 +1718,7 @@ binascii_a2b_base32_impl(PyObject *module, Py_buffer *data, int padded, if (padded && octa_pos != 0 && octa_pos + pads < 8) { state = get_binascii_state(module); if (state) { - PyErr_SetString(state->Error, "Incorrect padding"); + PyErr_SetString(state->Incomplete, "Incorrect padding"); } goto error; } @@ -2212,7 +2212,7 @@ binascii_a2b_hex_impl(PyObject *module, Py_buffer *hexstr, if (pair_pos) { state = get_binascii_state(module); if (state) { - PyErr_SetString(state->Error, "Odd number of hexadecimal digits"); + PyErr_SetString(state->Incomplete, "Odd number of hexadecimal digits"); } goto error; } @@ -2570,7 +2570,7 @@ binascii_exec(PyObject *module) return -1; } - state->Incomplete = PyErr_NewException("binascii.Incomplete", NULL, NULL); + state->Incomplete = PyErr_NewException("binascii.Incomplete", state->Error, NULL); if (PyModule_AddObjectRef(module, "Incomplete", state->Incomplete) < 0) { return -1; }