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