1import unittest 2 3import sys, io, subprocess 4import quopri 5 6 7 8ENCSAMPLE = b"""\ 9Here's a bunch of special=20 10 11=A1=A2=A3=A4=A5=A6=A7=A8=A9 12=AA=AB=AC=AD=AE=AF=B0=B1=B2=B3 13=B4=B5=B6=B7=B8=B9=BA=BB=BC=BD=BE 14=BF=C0=C1=C2=C3=C4=C5=C6 15=C7=C8=C9=CA=CB=CC=CD=CE=CF 16=D0=D1=D2=D3=D4=D5=D6=D7 17=D8=D9=DA=DB=DC=DD=DE=DF 18=E0=E1=E2=E3=E4=E5=E6=E7 19=E8=E9=EA=EB=EC=ED=EE=EF 20=F0=F1=F2=F3=F4=F5=F6=F7 21=F8=F9=FA=FB=FC=FD=FE=FF 22 23characters... have fun! 24""" 25 26# First line ends with a space 27DECSAMPLE = b"Here's a bunch of special \n" + \ 28b"""\ 29 30\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9 31\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3 32\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe 33\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6 34\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf 35\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7 36\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf 37\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7 38\xe8\xe9\xea\xeb\xec\xed\xee\xef 39\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7 40\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff 41 42characters... have fun! 43""" 44 45 46def withpythonimplementation(testfunc): 47 def newtest(self): 48 # Test default implementation 49 testfunc(self) 50 # Test Python implementation 51 if quopri.b2a_qp is not None or quopri.a2b_qp is not None: 52 oldencode = quopri.b2a_qp 53 olddecode = quopri.a2b_qp 54 try: 55 quopri.b2a_qp = None 56 quopri.a2b_qp = None 57 testfunc(self) 58 finally: 59 quopri.b2a_qp = oldencode 60 quopri.a2b_qp = olddecode 61 newtest.__name__ = testfunc.__name__ 62 return newtest 63 64class QuopriTestCase(unittest.TestCase): 65 # Each entry is a tuple of (plaintext, encoded string). These strings are 66 # used in the "quotetabs=0" tests. 67 STRINGS = ( 68 # Some normal strings 69 (b'hello', b'hello'), 70 (b'''hello 71 there 72 world''', b'''hello 73 there 74 world'''), 75 (b'''hello 76 there 77 world 78''', b'''hello 79 there 80 world 81'''), 82 (b'\201\202\203', b'=81=82=83'), 83 # Add some trailing MUST QUOTE strings 84 (b'hello ', b'hello=20'), 85 (b'hello\t', b'hello=09'), 86 # Some long lines. First, a single line of 108 characters 87 (b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\xd8\xd9\xda\xdb\xdc\xdd\xde\xdfxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 88 b'''xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=D8=D9=DA=DB=DC=DD=DE=DFx= 89xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'''), 90 # A line of exactly 76 characters, no soft line break should be needed 91 (b'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy', 92 b'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'), 93 # A line of 77 characters, forcing a soft line break at position 75, 94 # and a second line of exactly 2 characters (because the soft line 95 # break `=' sign counts against the line length limit). 96 (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', 97 b'''zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz= 98zz'''), 99 # A line of 151 characters, forcing a soft line break at position 75, 100 # with a second line of exactly 76 characters and no trailing = 101 (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', 102 b'''zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz= 103zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'''), 104 # A string containing a hard line break, but which the first line is 105 # 151 characters and the second line is exactly 76 characters. This 106 # should leave us with three lines, the first which has a soft line 107 # break, and which the second and third do not. 108 (b'''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy 109zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz''', 110 b'''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy= 111yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy 112zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'''), 113 # Now some really complex stuff ;) 114 (DECSAMPLE, ENCSAMPLE), 115 ) 116 117 # These are used in the "quotetabs=1" tests. 118 ESTRINGS = ( 119 (b'hello world', b'hello=20world'), 120 (b'hello\tworld', b'hello=09world'), 121 ) 122 123 # These are used in the "header=1" tests. 124 HSTRINGS = ( 125 (b'hello world', b'hello_world'), 126 (b'hello_world', b'hello=5Fworld'), 127 ) 128 129 @withpythonimplementation 130 def test_encodestring(self): 131 for p, e in self.STRINGS: 132 self.assertEqual(quopri.encodestring(p), e) 133 134 @withpythonimplementation 135 def test_decodestring(self): 136 for p, e in self.STRINGS: 137 self.assertEqual(quopri.decodestring(e), p) 138 139 @withpythonimplementation 140 def test_decodestring_double_equals(self): 141 # Issue 21511 - Ensure that byte string is compared to byte string 142 # instead of int byte value 143 decoded_value, encoded_value = (b"123=four", b"123==four") 144 self.assertEqual(quopri.decodestring(encoded_value), decoded_value) 145 146 @withpythonimplementation 147 def test_idempotent_string(self): 148 for p, e in self.STRINGS: 149 self.assertEqual(quopri.decodestring(quopri.encodestring(e)), e) 150 151 @withpythonimplementation 152 def test_encode(self): 153 for p, e in self.STRINGS: 154 infp = io.BytesIO(p) 155 outfp = io.BytesIO() 156 quopri.encode(infp, outfp, quotetabs=False) 157 self.assertEqual(outfp.getvalue(), e) 158 159 @withpythonimplementation 160 def test_decode(self): 161 for p, e in self.STRINGS: 162 infp = io.BytesIO(e) 163 outfp = io.BytesIO() 164 quopri.decode(infp, outfp) 165 self.assertEqual(outfp.getvalue(), p) 166 167 @withpythonimplementation 168 def test_embedded_ws(self): 169 for p, e in self.ESTRINGS: 170 self.assertEqual(quopri.encodestring(p, quotetabs=True), e) 171 self.assertEqual(quopri.decodestring(e), p) 172 173 @withpythonimplementation 174 def test_encode_header(self): 175 for p, e in self.HSTRINGS: 176 self.assertEqual(quopri.encodestring(p, header=True), e) 177 178 @withpythonimplementation 179 def test_decode_header(self): 180 for p, e in self.HSTRINGS: 181 self.assertEqual(quopri.decodestring(e, header=True), p) 182 183 def test_scriptencode(self): 184 (p, e) = self.STRINGS[-1] 185 process = subprocess.Popen([sys.executable, "-mquopri"], 186 stdin=subprocess.PIPE, stdout=subprocess.PIPE) 187 self.addCleanup(process.stdout.close) 188 cout, cerr = process.communicate(p) 189 # On Windows, Python will output the result to stdout using 190 # CRLF, as the mode of stdout is text mode. To compare this 191 # with the expected result, we need to do a line-by-line comparison. 192 cout = cout.decode('latin-1').splitlines() 193 e = e.decode('latin-1').splitlines() 194 assert len(cout)==len(e) 195 for i in range(len(cout)): 196 self.assertEqual(cout[i], e[i]) 197 self.assertEqual(cout, e) 198 199 def test_scriptdecode(self): 200 (p, e) = self.STRINGS[-1] 201 process = subprocess.Popen([sys.executable, "-mquopri", "-d"], 202 stdin=subprocess.PIPE, stdout=subprocess.PIPE) 203 self.addCleanup(process.stdout.close) 204 cout, cerr = process.communicate(e) 205 cout = cout.decode('latin-1') 206 p = p.decode('latin-1') 207 self.assertEqual(cout.splitlines(), p.splitlines()) 208 209if __name__ == "__main__": 210 unittest.main() 211