Skip to content

Commit 8bf049e

Browse files
committed
gh-133896: Refactor quopri to always use binascii
1 parent 13cb8ca commit 8bf049e

File tree

2 files changed

+10
-133
lines changed

2 files changed

+10
-133
lines changed

Lib/quopri.py

Lines changed: 10 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@
22

33
# (Dec 1991 version).
44

5+
from binascii import a2b_qp, b2a_qp
6+
57
__all__ = ["encode", "decode", "encodestring", "decodestring"]
68

79
ESCAPE = b'='
810
MAXLINESIZE = 76
911
HEX = b'0123456789ABCDEF'
1012
EMPTYSTRING = b''
1113

12-
try:
13-
from binascii import a2b_qp, b2a_qp
14-
except ImportError:
15-
a2b_qp = None
16-
b2a_qp = None
17-
1814

1915
def needsquoting(c, quotetabs, header):
2016
"""Decide whether a particular byte ordinal needs to be quoted.
@@ -47,118 +43,26 @@ def encode(input, output, quotetabs, header=False):
4743
line-ending tabs and spaces are always encoded, as per RFC 1521.
4844
The 'header' flag indicates whether we are encoding spaces as _ as per RFC
4945
1522."""
46+
data = input.read()
47+
odata = b2a_qp(data, quotetabs=quotetabs, header=header)
48+
output.write(odata)
5049

51-
if b2a_qp is not None:
52-
data = input.read()
53-
odata = b2a_qp(data, quotetabs=quotetabs, header=header)
54-
output.write(odata)
55-
return
56-
57-
def write(s, output=output, lineEnd=b'\n'):
58-
# RFC 1521 requires that the line ending in a space or tab must have
59-
# that trailing character encoded.
60-
if s and s[-1:] in b' \t':
61-
output.write(s[:-1] + quote(s[-1:]) + lineEnd)
62-
elif s == b'.':
63-
output.write(quote(s) + lineEnd)
64-
else:
65-
output.write(s + lineEnd)
66-
67-
prevline = None
68-
while line := input.readline():
69-
outline = []
70-
# Strip off any readline induced trailing newline
71-
stripped = b''
72-
if line[-1:] == b'\n':
73-
line = line[:-1]
74-
stripped = b'\n'
75-
# Calculate the un-length-limited encoded line
76-
for c in line:
77-
c = bytes((c,))
78-
if needsquoting(c, quotetabs, header):
79-
c = quote(c)
80-
if header and c == b' ':
81-
outline.append(b'_')
82-
else:
83-
outline.append(c)
84-
# First, write out the previous line
85-
if prevline is not None:
86-
write(prevline)
87-
# Now see if we need any soft line breaks because of RFC-imposed
88-
# length limitations. Then do the thisline->prevline dance.
89-
thisline = EMPTYSTRING.join(outline)
90-
while len(thisline) > MAXLINESIZE:
91-
# Don't forget to include the soft line break `=' sign in the
92-
# length calculation!
93-
write(thisline[:MAXLINESIZE-1], lineEnd=b'=\n')
94-
thisline = thisline[MAXLINESIZE-1:]
95-
# Write out the current line
96-
prevline = thisline
97-
# Write out the last line, without a trailing newline
98-
if prevline is not None:
99-
write(prevline, lineEnd=stripped)
10050

10151
def encodestring(s, quotetabs=False, header=False):
102-
if b2a_qp is not None:
103-
return b2a_qp(s, quotetabs=quotetabs, header=header)
104-
from io import BytesIO
105-
infp = BytesIO(s)
106-
outfp = BytesIO()
107-
encode(infp, outfp, quotetabs, header)
108-
return outfp.getvalue()
109-
52+
return b2a_qp(s, quotetabs=quotetabs, header=header)
11053

11154

11255
def decode(input, output, header=False):
11356
"""Read 'input', apply quoted-printable decoding, and write to 'output'.
11457
'input' and 'output' are binary file objects.
11558
If 'header' is true, decode underscore as space (per RFC 1522)."""
59+
data = input.read()
60+
odata = a2b_qp(data, header=header)
61+
output.write(odata)
11662

117-
if a2b_qp is not None:
118-
data = input.read()
119-
odata = a2b_qp(data, header=header)
120-
output.write(odata)
121-
return
122-
123-
new = b''
124-
while line := input.readline():
125-
i, n = 0, len(line)
126-
if n > 0 and line[n-1:n] == b'\n':
127-
partial = 0; n = n-1
128-
# Strip trailing whitespace
129-
while n > 0 and line[n-1:n] in b" \t\r":
130-
n = n-1
131-
else:
132-
partial = 1
133-
while i < n:
134-
c = line[i:i+1]
135-
if c == b'_' and header:
136-
new = new + b' '; i = i+1
137-
elif c != ESCAPE:
138-
new = new + c; i = i+1
139-
elif i+1 == n and not partial:
140-
partial = 1; break
141-
elif i+1 < n and line[i+1:i+2] == ESCAPE:
142-
new = new + ESCAPE; i = i+2
143-
elif i+2 < n and ishex(line[i+1:i+2]) and ishex(line[i+2:i+3]):
144-
new = new + bytes((unhex(line[i+1:i+3]),)); i = i+3
145-
else: # Bad escape sequence -- leave it in
146-
new = new + c; i = i+1
147-
if not partial:
148-
output.write(new + b'\n')
149-
new = b''
150-
if new:
151-
output.write(new)
15263

15364
def decodestring(s, header=False):
154-
if a2b_qp is not None:
155-
return a2b_qp(s, header=header)
156-
from io import BytesIO
157-
infp = BytesIO(s)
158-
outfp = BytesIO()
159-
decode(infp, outfp, header=header)
160-
return outfp.getvalue()
161-
65+
return a2b_qp(s, header=header)
16266

16367

16468
# Other helper functions

Lib/test/test_quopri.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,6 @@
4444
"""
4545

4646

47-
def withpythonimplementation(testfunc):
48-
def newtest(self):
49-
# Test default implementation
50-
testfunc(self)
51-
# Test Python implementation
52-
if quopri.b2a_qp is not None or quopri.a2b_qp is not None:
53-
oldencode = quopri.b2a_qp
54-
olddecode = quopri.a2b_qp
55-
try:
56-
quopri.b2a_qp = None
57-
quopri.a2b_qp = None
58-
testfunc(self)
59-
finally:
60-
quopri.b2a_qp = oldencode
61-
quopri.a2b_qp = olddecode
62-
newtest.__name__ = testfunc.__name__
63-
return newtest
64-
6547
class QuopriTestCase(unittest.TestCase):
6648
# Each entry is a tuple of (plaintext, encoded string). These strings are
6749
# used in the "quotetabs=0" tests.
@@ -127,56 +109,47 @@ class QuopriTestCase(unittest.TestCase):
127109
(b'hello_world', b'hello=5Fworld'),
128110
)
129111

130-
@withpythonimplementation
131112
def test_encodestring(self):
132113
for p, e in self.STRINGS:
133114
self.assertEqual(quopri.encodestring(p), e)
134115

135-
@withpythonimplementation
136116
def test_decodestring(self):
137117
for p, e in self.STRINGS:
138118
self.assertEqual(quopri.decodestring(e), p)
139119

140-
@withpythonimplementation
141120
def test_decodestring_double_equals(self):
142121
# Issue 21511 - Ensure that byte string is compared to byte string
143122
# instead of int byte value
144123
decoded_value, encoded_value = (b"123=four", b"123==four")
145124
self.assertEqual(quopri.decodestring(encoded_value), decoded_value)
146125

147-
@withpythonimplementation
148126
def test_idempotent_string(self):
149127
for p, e in self.STRINGS:
150128
self.assertEqual(quopri.decodestring(quopri.encodestring(e)), e)
151129

152-
@withpythonimplementation
153130
def test_encode(self):
154131
for p, e in self.STRINGS:
155132
infp = io.BytesIO(p)
156133
outfp = io.BytesIO()
157134
quopri.encode(infp, outfp, quotetabs=False)
158135
self.assertEqual(outfp.getvalue(), e)
159136

160-
@withpythonimplementation
161137
def test_decode(self):
162138
for p, e in self.STRINGS:
163139
infp = io.BytesIO(e)
164140
outfp = io.BytesIO()
165141
quopri.decode(infp, outfp)
166142
self.assertEqual(outfp.getvalue(), p)
167143

168-
@withpythonimplementation
169144
def test_embedded_ws(self):
170145
for p, e in self.ESTRINGS:
171146
self.assertEqual(quopri.encodestring(p, quotetabs=True), e)
172147
self.assertEqual(quopri.decodestring(e), p)
173148

174-
@withpythonimplementation
175149
def test_encode_header(self):
176150
for p, e in self.HSTRINGS:
177151
self.assertEqual(quopri.encodestring(p, header=True), e)
178152

179-
@withpythonimplementation
180153
def test_decode_header(self):
181154
for p, e in self.HSTRINGS:
182155
self.assertEqual(quopri.decodestring(e, header=True), p)

0 commit comments

Comments
 (0)