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
116 changes: 10 additions & 106 deletions Lib/quopri.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@

# (Dec 1991 version).

from binascii import a2b_qp, b2a_qp

__all__ = ["encode", "decode", "encodestring", "decodestring"]

ESCAPE = b'='
MAXLINESIZE = 76
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.
Expand Down Expand Up @@ -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
Expand Down
27 changes: 0 additions & 27 deletions Lib/test/test_quopri.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -127,56 +109,47 @@ 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)
outfp = io.BytesIO()
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)
outfp = io.BytesIO()
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)
Expand Down
Loading