diff --git a/Doc/library/binascii.rst b/Doc/library/binascii.rst index ceb80a35a1a76bb..63fb58ab9283da0 100644 --- a/Doc/library/binascii.rst +++ b/Doc/library/binascii.rst @@ -283,11 +283,16 @@ The :mod:`!binascii` module defines the following functions: .. versionadded:: 3.15 -.. function:: a2b_qp(data, header=False) +.. function:: a2b_qp(data, header=False, strip_ws=False) Convert a block of quoted-printable data back to binary and return the binary data. More than one line may be passed at a time. If the optional argument *header* is present and true, underscores will be decoded as spaces. + If the optional argument *strip_ws* is true, + trailing whitespace is stripped from each line, as required by :rfc:`2045`. + + .. versionchanged:: next + Added the *strip_ws* parameter. .. function:: b2a_qp(data, quotetabs=False, istext=True, header=False) diff --git a/Doc/library/email.compat32-message.rst b/Doc/library/email.compat32-message.rst index 5754c2b65b239f9..620f5c3fd870719 100644 --- a/Doc/library/email.compat32-message.rst +++ b/Doc/library/email.compat32-message.rst @@ -214,6 +214,14 @@ Here are the methods of the :class:`Message` class: defect property (:class:`~email.errors.InvalidBase64PaddingDefect` or :class:`~email.errors.InvalidBase64CharactersDefect`, respectively). + .. note:: + + A ``quoted-printable`` payload is decoded without stripping + trailing whitespace, contrary to :rfc:`2045` but matching + common mail clients. + Use :func:`binascii.a2b_qp` with ``strip_ws=True`` + (or ``email.quoprimime.decode``) for RFC-compliant decoding. + When *decode* is ``False`` (the default) the body is returned as a string without decoding the :mailheader:`Content-Transfer-Encoding`. However, for a :mailheader:`Content-Transfer-Encoding` of 8bit, an attempt is made diff --git a/Doc/library/quopri.rst b/Doc/library/quopri.rst index 977cb08d836afe3..92444c33ec1da70 100644 --- a/Doc/library/quopri.rst +++ b/Doc/library/quopri.rst @@ -20,7 +20,7 @@ few nonprintable characters; the base64 encoding scheme available via the :mod:`base64` module is more compact if there are many such characters, as when sending a graphics file. -.. function:: decode(input, output, header=False) +.. function:: decode(input, output, header=False, strip_ws=False) Decode the contents of the *input* file and write the resulting decoded binary data to the *output* file. *input* and *output* must be :term:`binary file objects @@ -28,6 +28,11 @@ sending a graphics file. will be decoded as space. This is used to decode "Q"-encoded headers as described in :rfc:`1522`: "MIME (Multipurpose Internet Mail Extensions) Part Two: Message Header Extensions for Non-ASCII Text". + If the optional argument *strip_ws* is true, + trailing whitespace is stripped from each line, as required by :rfc:`2045`. + + .. versionchanged:: next + Added the *strip_ws* parameter. .. function:: encode(input, output, quotetabs, header=False) @@ -43,11 +48,14 @@ sending a graphics file. as underscores as per :rfc:`1522`. -.. function:: decodestring(s, header=False) +.. function:: decodestring(s, header=False, strip_ws=False) Like :func:`decode`, except that it accepts a source :class:`bytes` and returns the corresponding decoded :class:`bytes`. + .. versionchanged:: next + Added the *strip_ws* parameter. + .. function:: encodestring(s, quotetabs=False, header=False) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index e215d4ddfdf41b7..baf7499e200b6db 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -270,6 +270,16 @@ os (Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.) +quopri +------ + +* :func:`quopri.decode`, :func:`quopri.decodestring` and + :func:`binascii.a2b_qp` gained a *strip_ws* parameter. When true, trailing + whitespace is stripped from each line while decoding, as required by + :rfc:`2045` for a quoted-printable body. + (Contributed by Serhiy Storchaka in :gh:`62222`.) + + re -- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 7d952a1e52561a6..d47d94eac17179b 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -2103,6 +2103,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strict)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strict_mode)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(string)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strip_ws)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sub_key)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(subcalls)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(symmetric_difference_update)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 8a8bbc3b6d05bf7..4d2d5365232f303 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -826,6 +826,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(strict) STRUCT_FOR_ID(strict_mode) STRUCT_FOR_ID(string) + STRUCT_FOR_ID(strip_ws) STRUCT_FOR_ID(sub_key) STRUCT_FOR_ID(subcalls) STRUCT_FOR_ID(symmetric_difference_update) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 366d2d300fb4780..a6d411a89535ce8 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -2101,6 +2101,7 @@ extern "C" { INIT_ID(strict), \ INIT_ID(strict_mode), \ INIT_ID(string), \ + INIT_ID(strip_ws), \ INIT_ID(sub_key), \ INIT_ID(subcalls), \ INIT_ID(symmetric_difference_update), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 00d6297432b5fca..969947c9ae8a6e6 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -3084,6 +3084,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(strip_ws); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(sub_key); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/email/quoprimime.py b/Lib/email/quoprimime.py index bc53b3768213102..1c827badb7783b9 100644 --- a/Lib/email/quoprimime.py +++ b/Lib/email/quoprimime.py @@ -229,10 +229,14 @@ def body_encode(body, maxlinelen=76, eol=NL): # BAW: I'm not sure if the intent was for the signature of this function to be # the same as base64MIME.decode() or not... -def decode(encoded, eol=NL): +def decode(encoded, eol=NL, strip_ws=True): """Decode a quoted-printable string. Lines are separated with eol, which defaults to \\n. + + If strip_ws is true (the default), whitespace at the end of a line is + stripped, as required by RFC 2045 when decoding a quoted-printable body. + Pass strip_ws=False to keep it. """ if not encoded: return encoded @@ -242,7 +246,8 @@ def decode(encoded, eol=NL): decoded = '' for line in encoded.splitlines(): - line = line.rstrip() + if strip_ws: + line = line.rstrip() if not line: decoded += eol continue diff --git a/Lib/quopri.py b/Lib/quopri.py index 129fd2f5c7c28a8..9d69a897b2ea20d 100644 --- a/Lib/quopri.py +++ b/Lib/quopri.py @@ -109,14 +109,16 @@ def encodestring(s, quotetabs=False, header=False): -def decode(input, output, header=False): +def decode(input, output, header=False, strip_ws=False): """Read 'input', apply quoted-printable decoding, and write to 'output'. 'input' and 'output' are binary file objects. - If 'header' is true, decode underscore as space (per RFC 1522).""" + If 'header' is true, decode underscore as space (per RFC 1522). + If 'strip_ws' is true, strip whitespace at the end of a line (per + RFC 2045).""" if a2b_qp is not None: data = input.read() - odata = a2b_qp(data, header=header) + odata = a2b_qp(data, header=header, strip_ws=strip_ws) output.write(odata) return @@ -125,11 +127,19 @@ def decode(input, output, header=False): i, n = 0, len(line) if n > 0 and line[n-1:n] == b'\n': partial = 0; n = n-1 - # Strip trailing whitespace - while n > 0 and line[n-1:n] in b" \t\r": - n = n-1 + # Separate off the line ending (keeping it to re-add after + # decoding) so that a trailing "=" -- possibly before the "\r" of + # a "\r\n" pair -- is recognized as a soft line break. + if n > 0 and line[n-1:n] == b'\r': + n = n-1; eol = b'\r\n' + else: + eol = b'\n' else: - partial = 1 + partial = 1; eol = b'' + if strip_ws: + # Strip trailing whitespace (RFC 2045). + while n > 0 and line[n-1:n] in b" \t": + n = n-1 while i < n: c = line[i:i+1] if c == b'_' and header: @@ -138,25 +148,23 @@ def decode(input, output, header=False): new = new + c; i = i+1 elif i+1 == n and not partial: partial = 1; break - elif i+1 < n and line[i+1:i+2] == ESCAPE: - new = new + ESCAPE; i = i+2 elif i+2 < n and ishex(line[i+1:i+2]) and ishex(line[i+2:i+3]): new = new + bytes((unhex(line[i+1:i+3]),)); i = i+3 else: # Bad escape sequence -- leave it in new = new + c; i = i+1 if not partial: - output.write(new + b'\n') + output.write(new + eol) new = b'' if new: output.write(new) -def decodestring(s, header=False): +def decodestring(s, header=False, strip_ws=False): if a2b_qp is not None: - return a2b_qp(s, header=header) + return a2b_qp(s, header=header, strip_ws=strip_ws) from io import BytesIO infp = BytesIO(s) outfp = BytesIO() - decode(infp, outfp, header=header) + decode(infp, outfp, header=header, strip_ws=strip_ws) return outfp.getvalue() diff --git a/Lib/test/test_binascii.py b/Lib/test/test_binascii.py index cedbdc61f18f341..77ea77ed37bdc59 100644 --- a/Lib/test/test_binascii.py +++ b/Lib/test/test_binascii.py @@ -1418,6 +1418,10 @@ def test_qp(self): self.assertEqual(a2b_qp(type2test(b"=")), b"") self.assertEqual(a2b_qp(type2test(b"= ")), b"= ") self.assertEqual(a2b_qp(type2test(b"==")), b"=") + # A stray "=" is left in place and the next character is rescanned, + # so "=41" after it is decoded as a fresh escape (gh-62222). + self.assertEqual(a2b_qp(type2test(b"==41")), b"=A") + self.assertEqual(a2b_qp(type2test(b"==g")), b"==g") self.assertEqual(a2b_qp(type2test(b"=\nAB")), b"AB") self.assertEqual(a2b_qp(type2test(b"=\r\nAB")), b"AB") self.assertEqual(a2b_qp(type2test(b"=\rAB")), b"") # ? @@ -1431,6 +1435,21 @@ def test_qp(self): self.assertEqual(a2b_qp(type2test(b'_')), b'_') self.assertEqual(a2b_qp(type2test(b'_'), header=True), b' ') + # strip_ws strips whitespace at the end of a line (RFC 2045), but + # leaves whitespace that was encoded (=20/=09) untouched. By default + # trailing whitespace is kept. + self.assertEqual(a2b_qp(type2test(b"foo \n")), b"foo \n") + self.assertEqual(a2b_qp(type2test(b"foo \n"), strip_ws=True), b"foo\n") + self.assertEqual(a2b_qp(type2test(b"foo "), strip_ws=True), b"foo") + self.assertEqual(a2b_qp(type2test(b"a b \nc\n"), strip_ws=True), b"a b\nc\n") + self.assertEqual(a2b_qp(type2test(b"a=20 \n"), strip_ws=True), b"a \n") + self.assertEqual(a2b_qp(type2test(b"= \n"), strip_ws=True), b"") + self.assertEqual(a2b_qp(type2test(b"foo =\n"), strip_ws=True), b"foo ") + self.assertEqual(a2b_qp(type2test(b"foo \r\n"), strip_ws=True), b"foo\r\n") + # A bare CR is not a line separator (RFC 2045: CR occurs only in CRLF), + # so whitespace before it is kept. + self.assertEqual(a2b_qp(type2test(b"foo \rbar"), strip_ws=True), b"foo \rbar") + self.assertRaises(TypeError, b2a_qp, foo="bar") self.assertEqual(a2b_qp(type2test(b"=00\r\n=00")), b"\x00\r\n\x00") self.assertEqual(b2a_qp(type2test(b"\xff\r\n\xff\n\xff")), diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index 19555d87085e176..b0816d1c08cd383 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -4813,6 +4813,14 @@ def test_decode_one_line_trailing_spaces(self): def test_decode_two_lines_trailing_spaces(self): self._test_decode('hello \r\nworld \r\n', 'hello\nworld\n') + def test_decode_keep_trailing_spaces(self): + # strip_ws=False keeps trailing whitespace (the default strips it). + self.assertEqual(quoprimime.decode('hello \r\n', strip_ws=False), + 'hello \n') + self.assertEqual(quoprimime.decode('hello \r\nworld \r\n', + strip_ws=False), + 'hello \nworld \n') + def test_decode_quoted_word(self): self._test_decode('=22quoted=20words=22', '"quoted words"') diff --git a/Lib/test/test_quopri.py b/Lib/test/test_quopri.py index 152d1858dcdd242..2e013e9e4c80b31 100644 --- a/Lib/test/test_quopri.py +++ b/Lib/test/test_quopri.py @@ -140,8 +140,10 @@ def test_decodestring(self): @withpythonimplementation def test_decodestring_double_equals(self): # Issue 21511 - Ensure that byte string is compared to byte string - # instead of int byte value - decoded_value, encoded_value = (b"123=four", b"123==four") + # instead of int byte value. + # A stray "=" not starting a valid escape is left in place, so the + # following "=four" is decoded as a fresh (invalid) escape too. + decoded_value, encoded_value = (b"123==four", b"123==four") self.assertEqual(quopri.decodestring(encoded_value), decoded_value) @withpythonimplementation @@ -171,6 +173,30 @@ def test_embedded_ws(self): self.assertEqual(quopri.encodestring(p, quotetabs=True), e) self.assertEqual(quopri.decodestring(e), p) + @withpythonimplementation + def test_decode_strip_ws(self): + # By default trailing whitespace is kept; with strip_ws it is + # removed at the end of a line (RFC 2045), while encoded whitespace + # (=20/=09) is preserved. + self.assertEqual(quopri.decodestring(b"foo \n"), b"foo \n") + self.assertEqual(quopri.decodestring(b"foo \n", strip_ws=True), b"foo\n") + self.assertEqual(quopri.decodestring(b"a b \nc\n", strip_ws=True), b"a b\nc\n") + self.assertEqual(quopri.decodestring(b"a=20 \n", strip_ws=True), b"a \n") + self.assertEqual(quopri.decodestring(b"= \n", strip_ws=True), b"") + self.assertEqual(quopri.decodestring(b"foo =\n", strip_ws=True), b"foo ") + self.assertEqual(quopri.decodestring(b"foo \r\n", strip_ws=True), b"foo\r\n") + # A bare CR is not a line separator, so whitespace before it is kept. + self.assertEqual(quopri.decodestring(b"foo \rbar", strip_ws=True), b"foo \rbar") + + @withpythonimplementation + def test_decode_soft_line_break(self): + # A "=" at the end of a line is a soft line break, for both "\n" and + # "\r\n" line endings; other line endings are preserved as-is. + self.assertEqual(quopri.decodestring(b"=\nAB"), b"AB") + self.assertEqual(quopri.decodestring(b"=\r\nAB"), b"AB") + self.assertEqual(quopri.decodestring(b"foo=\r\nbar"), b"foobar") + self.assertEqual(quopri.decodestring(b"foo\r\nbar"), b"foo\r\nbar") + @withpythonimplementation def test_encode_header(self): for p, e in self.HSTRINGS: diff --git a/Misc/NEWS.d/next/Library/2026-06-30-08-39-07.gh-issue-62222.Zq7x2P.rst b/Misc/NEWS.d/next/Library/2026-06-30-08-39-07.gh-issue-62222.Zq7x2P.rst new file mode 100644 index 000000000000000..a17c9d74d6f2945 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-30-08-39-07.gh-issue-62222.Zq7x2P.rst @@ -0,0 +1,9 @@ +:func:`binascii.a2b_qp` and :mod:`quopri` now leave a stray ``=`` (one not +followed by two hexadecimal digits or a soft line break) in place and rescan +the following character, instead of consuming and discarding it; for example +``b'==41'`` now decodes to ``b'=A'``, matching the :mod:`email` package and +other quoted-printable decoders. A *strip_ws* parameter is also added to +:func:`binascii.a2b_qp`, :func:`quopri.decode`, :func:`quopri.decodestring` +and ``email.quoprimime.decode`` to strip trailing whitespace from each line +while decoding, as required by :rfc:`2045`; it is false by default everywhere +except ``email.quoprimime.decode``, preserving every existing behavior. diff --git a/Modules/binascii.c b/Modules/binascii.c index 0e7af135a6f6ce4..2cef345f54aff2a 100644 --- a/Modules/binascii.c +++ b/Modules/binascii.c @@ -2373,28 +2373,80 @@ binascii.a2b_qp data: ascii_buffer header: bool = False + strip_ws: bool = False Decode a string of qp-encoded data. + +If strip_ws is true, whitespace at the end of a line is stripped, as +required by RFC 2045 when decoding a quoted-printable body. [clinic start generated code]*/ static PyObject * -binascii_a2b_qp_impl(PyObject *module, Py_buffer *data, int header) -/*[clinic end generated code: output=e99f7846cfb9bc53 input=bdfb31598d4e47b9]*/ +binascii_a2b_qp_impl(PyObject *module, Py_buffer *data, int header, + int strip_ws) +/*[clinic end generated code: output=7e9431376a28709d input=f637bdfb25d485a8]*/ { Py_ssize_t in, out; char ch; const unsigned char *ascii_data; unsigned char *odata; + unsigned char *stripped = NULL; Py_ssize_t datalen = 0; PyObject *rv; ascii_data = data->buf; datalen = data->len; + /* Strip trailing space and tab before each line break and at the end of + * the data (RFC 2045). Done before decoding, so that a "=" left at the + * end of a line becomes a soft line break and a "=20" is preserved. + */ + if (strip_ws) { + Py_ssize_t i = 0, j = 0, start = 0; + while (i < datalen) { + if (ascii_data[i] != ' ' && ascii_data[i] != '\t') { + i++; + continue; + } + /* Find the run of space and tab. */ + Py_ssize_t k = i; + while (k < datalen && + (ascii_data[k] == ' ' || ascii_data[k] == '\t')) { + k++; + } + /* Drop it if it ends a line: before "\n", "\r\n" or the end of the + * data, but not a bare "\r" (RFC 2045: CR occurs only in CRLF). + */ + if (k == datalen || ascii_data[k] == '\n' || + (ascii_data[k] == '\r' && k + 1 < datalen && + ascii_data[k+1] == '\n')) { + if (stripped == NULL) { + /* Allocate only once something is actually stripped. */ + stripped = (unsigned char *) PyMem_Malloc(datalen); + if (stripped == NULL) { + PyErr_NoMemory(); + return NULL; + } + } + memcpy(stripped + j, ascii_data + start, i - start); + j += i - start; + start = k; + } + i = k; + } + if (stripped != NULL) { + memcpy(stripped + j, ascii_data + start, datalen - start); + j += datalen - start; + ascii_data = stripped; + datalen = j; + } + } + /* We allocate the output same size as input, this is overkill. */ odata = (unsigned char *) PyMem_Calloc(1, datalen); if (odata == NULL) { + PyMem_Free(stripped); PyErr_NoMemory(); return NULL; } @@ -2411,11 +2463,6 @@ binascii_a2b_qp_impl(PyObject *module, Py_buffer *data, int header) } if (in < datalen) in++; } - else if (ascii_data[in] == '=') { - /* broken case from broken python qp */ - odata[out++] = '='; - in++; - } else if ((in + 1 < datalen) && ((ascii_data[in] >= 'A' && ascii_data[in] <= 'F') || (ascii_data[in] >= 'a' && ascii_data[in] <= 'f') || @@ -2446,6 +2493,7 @@ binascii_a2b_qp_impl(PyObject *module, Py_buffer *data, int header) } rv = PyBytes_FromStringAndSize((char *)odata, out); PyMem_Free(odata); + PyMem_Free(stripped); return rv; } diff --git a/Modules/clinic/binascii.c.h b/Modules/clinic/binascii.c.h index 29fa9e87de87c7a..8164da133d19d8f 100644 --- a/Modules/clinic/binascii.c.h +++ b/Modules/clinic/binascii.c.h @@ -1513,16 +1513,20 @@ binascii_unhexlify(PyObject *module, PyObject *const *args, Py_ssize_t nargs, Py } PyDoc_STRVAR(binascii_a2b_qp__doc__, -"a2b_qp($module, /, data, header=False)\n" +"a2b_qp($module, /, data, header=False, strip_ws=False)\n" "--\n" "\n" -"Decode a string of qp-encoded data."); +"Decode a string of qp-encoded data.\n" +"\n" +"If strip_ws is true, whitespace at the end of a line is stripped, as\n" +"required by RFC 2045 when decoding a quoted-printable body."); #define BINASCII_A2B_QP_METHODDEF \ {"a2b_qp", _PyCFunction_CAST(binascii_a2b_qp), METH_FASTCALL|METH_KEYWORDS, binascii_a2b_qp__doc__}, static PyObject * -binascii_a2b_qp_impl(PyObject *module, Py_buffer *data, int header); +binascii_a2b_qp_impl(PyObject *module, Py_buffer *data, int header, + int strip_ws); static PyObject * binascii_a2b_qp(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -1530,7 +1534,7 @@ binascii_a2b_qp(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 2 + #define NUM_KEYWORDS 3 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -1539,7 +1543,7 @@ binascii_a2b_qp(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(data), &_Py_ID(header), }, + .ob_item = { &_Py_ID(data), &_Py_ID(header), &_Py_ID(strip_ws), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -1548,20 +1552,21 @@ binascii_a2b_qp(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"data", "header", NULL}; + static const char * const _keywords[] = {"data", "header", "strip_ws", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "a2b_qp", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[2]; + PyObject *argsbuf[3]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; Py_buffer data = {NULL, NULL}; int header = 0; + int strip_ws = 0; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + /*minpos*/ 1, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!args) { goto exit; } @@ -1571,12 +1576,21 @@ binascii_a2b_qp(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj if (!noptargs) { goto skip_optional_pos; } - header = PyObject_IsTrue(args[1]); - if (header < 0) { + if (args[1]) { + header = PyObject_IsTrue(args[1]); + if (header < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } + } + strip_ws = PyObject_IsTrue(args[2]); + if (strip_ws < 0) { goto exit; } skip_optional_pos: - return_value = binascii_a2b_qp_impl(module, &data, header); + return_value = binascii_a2b_qp_impl(module, &data, header, strip_ws); exit: /* Cleanup for data */ @@ -1685,4 +1699,4 @@ binascii_b2a_qp(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj return return_value; } -/*[clinic end generated code: output=42dd48f323cbb118 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=202204ced5906f7a input=a9049054013a1b77]*/