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