Skip to content
Draft
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
7 changes: 6 additions & 1 deletion Doc/library/binascii.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------
Expand Down
16 changes: 8 additions & 8 deletions Lib/test/test_base64.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
45 changes: 29 additions & 16 deletions Lib/test/test_binascii.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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=')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -812,16 +820,21 @@ 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)

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-")
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make :exc:`binascii.Incomplete` a subclass of :exc:`binascii.Error` and use
it for incomplete data errors.
14 changes: 7 additions & 7 deletions Modules/binascii.c
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
Loading