From 8bf049effd4df672ce845b0a76484b48aae3560c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 11 May 2025 16:43:07 +0300 Subject: [PATCH] gh-133896: Refactor `quopri` to always use `binascii` --- Lib/quopri.py | 116 ++++------------------------------------ Lib/test/test_quopri.py | 27 ---------- 2 files changed, 10 insertions(+), 133 deletions(-) diff --git a/Lib/quopri.py b/Lib/quopri.py index 129fd2f5c7c28a..b78579d6261839 100644 --- a/Lib/quopri.py +++ b/Lib/quopri.py @@ -2,6 +2,8 @@ # (Dec 1991 version). +from binascii import a2b_qp, b2a_qp + __all__ = ["encode", "decode", "encodestring", "decodestring"] ESCAPE = b'=' @@ -9,12 +11,6 @@ HEX = b'0123456789ABCDEF' EMPTYSTRING = b'' -try: - from binascii import a2b_qp, b2a_qp -except ImportError: - a2b_qp = None - b2a_qp = None - def needsquoting(c, quotetabs, header): """Decide whether a particular byte ordinal needs to be quoted. @@ -47,118 +43,26 @@ def encode(input, output, quotetabs, header=False): line-ending tabs and spaces are always encoded, as per RFC 1521. The 'header' flag indicates whether we are encoding spaces as _ as per RFC 1522.""" + data = input.read() + odata = b2a_qp(data, quotetabs=quotetabs, header=header) + output.write(odata) - if b2a_qp is not None: - data = input.read() - odata = b2a_qp(data, quotetabs=quotetabs, header=header) - output.write(odata) - return - - def write(s, output=output, lineEnd=b'\n'): - # RFC 1521 requires that the line ending in a space or tab must have - # that trailing character encoded. - if s and s[-1:] in b' \t': - output.write(s[:-1] + quote(s[-1:]) + lineEnd) - elif s == b'.': - output.write(quote(s) + lineEnd) - else: - output.write(s + lineEnd) - - prevline = None - while line := input.readline(): - outline = [] - # Strip off any readline induced trailing newline - stripped = b'' - if line[-1:] == b'\n': - line = line[:-1] - stripped = b'\n' - # Calculate the un-length-limited encoded line - for c in line: - c = bytes((c,)) - if needsquoting(c, quotetabs, header): - c = quote(c) - if header and c == b' ': - outline.append(b'_') - else: - outline.append(c) - # First, write out the previous line - if prevline is not None: - write(prevline) - # Now see if we need any soft line breaks because of RFC-imposed - # length limitations. Then do the thisline->prevline dance. - thisline = EMPTYSTRING.join(outline) - while len(thisline) > MAXLINESIZE: - # Don't forget to include the soft line break `=' sign in the - # length calculation! - write(thisline[:MAXLINESIZE-1], lineEnd=b'=\n') - thisline = thisline[MAXLINESIZE-1:] - # Write out the current line - prevline = thisline - # Write out the last line, without a trailing newline - if prevline is not None: - write(prevline, lineEnd=stripped) def encodestring(s, quotetabs=False, header=False): - if b2a_qp is not None: - return b2a_qp(s, quotetabs=quotetabs, header=header) - from io import BytesIO - infp = BytesIO(s) - outfp = BytesIO() - encode(infp, outfp, quotetabs, header) - return outfp.getvalue() - + return b2a_qp(s, quotetabs=quotetabs, header=header) def decode(input, output, header=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).""" + data = input.read() + odata = a2b_qp(data, header=header) + output.write(odata) - if a2b_qp is not None: - data = input.read() - odata = a2b_qp(data, header=header) - output.write(odata) - return - - new = b'' - while line := input.readline(): - 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 - else: - partial = 1 - while i < n: - c = line[i:i+1] - if c == b'_' and header: - new = new + b' '; i = i+1 - elif c != ESCAPE: - 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') - new = b'' - if new: - output.write(new) def decodestring(s, header=False): - if a2b_qp is not None: - return a2b_qp(s, header=header) - from io import BytesIO - infp = BytesIO(s) - outfp = BytesIO() - decode(infp, outfp, header=header) - return outfp.getvalue() - + return a2b_qp(s, header=header) # Other helper functions diff --git a/Lib/test/test_quopri.py b/Lib/test/test_quopri.py index 152d1858dcdd24..db88150e5f0ea3 100644 --- a/Lib/test/test_quopri.py +++ b/Lib/test/test_quopri.py @@ -44,24 +44,6 @@ """ -def withpythonimplementation(testfunc): - def newtest(self): - # Test default implementation - testfunc(self) - # Test Python implementation - if quopri.b2a_qp is not None or quopri.a2b_qp is not None: - oldencode = quopri.b2a_qp - olddecode = quopri.a2b_qp - try: - quopri.b2a_qp = None - quopri.a2b_qp = None - testfunc(self) - finally: - quopri.b2a_qp = oldencode - quopri.a2b_qp = olddecode - newtest.__name__ = testfunc.__name__ - return newtest - class QuopriTestCase(unittest.TestCase): # Each entry is a tuple of (plaintext, encoded string). These strings are # used in the "quotetabs=0" tests. @@ -127,29 +109,24 @@ class QuopriTestCase(unittest.TestCase): (b'hello_world', b'hello=5Fworld'), ) - @withpythonimplementation def test_encodestring(self): for p, e in self.STRINGS: self.assertEqual(quopri.encodestring(p), e) - @withpythonimplementation def test_decodestring(self): for p, e in self.STRINGS: self.assertEqual(quopri.decodestring(e), p) - @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") self.assertEqual(quopri.decodestring(encoded_value), decoded_value) - @withpythonimplementation def test_idempotent_string(self): for p, e in self.STRINGS: self.assertEqual(quopri.decodestring(quopri.encodestring(e)), e) - @withpythonimplementation def test_encode(self): for p, e in self.STRINGS: infp = io.BytesIO(p) @@ -157,7 +134,6 @@ def test_encode(self): quopri.encode(infp, outfp, quotetabs=False) self.assertEqual(outfp.getvalue(), e) - @withpythonimplementation def test_decode(self): for p, e in self.STRINGS: infp = io.BytesIO(e) @@ -165,18 +141,15 @@ def test_decode(self): quopri.decode(infp, outfp) self.assertEqual(outfp.getvalue(), p) - @withpythonimplementation def test_embedded_ws(self): for p, e in self.ESTRINGS: self.assertEqual(quopri.encodestring(p, quotetabs=True), e) self.assertEqual(quopri.decodestring(e), p) - @withpythonimplementation def test_encode_header(self): for p, e in self.HSTRINGS: self.assertEqual(quopri.encodestring(p, header=True), e) - @withpythonimplementation def test_decode_header(self): for p, e in self.HSTRINGS: self.assertEqual(quopri.decodestring(e, header=True), p)