1"""
2Tests for uu module.
3Nick Mathewson
4"""
5
6import unittest
7from test.support import os_helper, warnings_helper
8
9uu = warnings_helper.import_deprecated("uu")
10
11import os
12import stat
13import sys
14import io
15
16plaintext = b"The symbols on top of your keyboard are !@#$%^&*()_+|~\n"
17
18encodedtext = b"""\
19M5&AE('-Y;6)O;',@;VX@=&]P(&]F('EO=7(@:V5Y8F]A<F0@87)E("% (R0E
20*7B8J*"E?*WQ^"@  """
21
22# Stolen from io.py
23class FakeIO(io.TextIOWrapper):
24    """Text I/O implementation using an in-memory buffer.
25
26    Can be a used as a drop-in replacement for sys.stdin and sys.stdout.
27    """
28
29    # XXX This is really slow, but fully functional
30
31    def __init__(self, initial_value="", encoding="utf-8",
32                 errors="strict", newline="\n"):
33        super(FakeIO, self).__init__(io.BytesIO(),
34                                     encoding=encoding,
35                                     errors=errors,
36                                     newline=newline)
37        self._encoding = encoding
38        self._errors = errors
39        if initial_value:
40            if not isinstance(initial_value, str):
41                initial_value = str(initial_value)
42            self.write(initial_value)
43            self.seek(0)
44
45    def getvalue(self):
46        self.flush()
47        return self.buffer.getvalue().decode(self._encoding, self._errors)
48
49
50def encodedtextwrapped(mode, filename, backtick=False):
51    if backtick:
52        res = (bytes("begin %03o %s\n" % (mode, filename), "ascii") +
53               encodedtext.replace(b' ', b'`') + b"\n`\nend\n")
54    else:
55        res = (bytes("begin %03o %s\n" % (mode, filename), "ascii") +
56               encodedtext + b"\n \nend\n")
57    return res
58
59class UUTest(unittest.TestCase):
60
61    def test_encode(self):
62        inp = io.BytesIO(plaintext)
63        out = io.BytesIO()
64        uu.encode(inp, out, "t1")
65        self.assertEqual(out.getvalue(), encodedtextwrapped(0o666, "t1"))
66        inp = io.BytesIO(plaintext)
67        out = io.BytesIO()
68        uu.encode(inp, out, "t1", 0o644)
69        self.assertEqual(out.getvalue(), encodedtextwrapped(0o644, "t1"))
70        inp = io.BytesIO(plaintext)
71        out = io.BytesIO()
72        uu.encode(inp, out, "t1", backtick=True)
73        self.assertEqual(out.getvalue(), encodedtextwrapped(0o666, "t1", True))
74        with self.assertRaises(TypeError):
75            uu.encode(inp, out, "t1", 0o644, True)
76
77    @os_helper.skip_unless_working_chmod
78    def test_decode(self):
79        for backtick in True, False:
80            inp = io.BytesIO(encodedtextwrapped(0o666, "t1", backtick=backtick))
81            out = io.BytesIO()
82            uu.decode(inp, out)
83            self.assertEqual(out.getvalue(), plaintext)
84            inp = io.BytesIO(
85                b"UUencoded files may contain many lines,\n" +
86                b"even some that have 'begin' in them.\n" +
87                encodedtextwrapped(0o666, "t1", backtick=backtick)
88            )
89            out = io.BytesIO()
90            uu.decode(inp, out)
91            self.assertEqual(out.getvalue(), plaintext)
92
93    def test_truncatedinput(self):
94        inp = io.BytesIO(b"begin 644 t1\n" + encodedtext)
95        out = io.BytesIO()
96        try:
97            uu.decode(inp, out)
98            self.fail("No exception raised")
99        except uu.Error as e:
100            self.assertEqual(str(e), "Truncated input file")
101
102    def test_missingbegin(self):
103        inp = io.BytesIO(b"")
104        out = io.BytesIO()
105        try:
106            uu.decode(inp, out)
107            self.fail("No exception raised")
108        except uu.Error as e:
109            self.assertEqual(str(e), "No valid begin line found in input file")
110
111    def test_garbage_padding(self):
112        # Issue #22406
113        encodedtext1 = (
114            b"begin 644 file\n"
115            # length 1; bits 001100 111111 111111 111111
116            b"\x21\x2C\x5F\x5F\x5F\n"
117            b"\x20\n"
118            b"end\n"
119        )
120        encodedtext2 = (
121            b"begin 644 file\n"
122            # length 1; bits 001100 111111 111111 111111
123            b"\x21\x2C\x5F\x5F\x5F\n"
124            b"\x60\n"
125            b"end\n"
126        )
127        plaintext = b"\x33"  # 00110011
128
129        for encodedtext in encodedtext1, encodedtext2:
130            with self.subTest("uu.decode()"):
131                inp = io.BytesIO(encodedtext)
132                out = io.BytesIO()
133                uu.decode(inp, out, quiet=True)
134                self.assertEqual(out.getvalue(), plaintext)
135
136            with self.subTest("uu_codec"):
137                import codecs
138                decoded = codecs.decode(encodedtext, "uu_codec")
139                self.assertEqual(decoded, plaintext)
140
141    def test_newlines_escaped(self):
142        # Test newlines are escaped with uu.encode
143        inp = io.BytesIO(plaintext)
144        out = io.BytesIO()
145        filename = "test.txt\n\roverflow.txt"
146        safefilename = b"test.txt\\n\\roverflow.txt"
147        uu.encode(inp, out, filename)
148        self.assertIn(safefilename, out.getvalue())
149
150    def test_no_directory_traversal(self):
151        relative_bad = b"""\
152begin 644 ../../../../../../../../tmp/test1
153$86)C"@``
154`
155end
156"""
157        with self.assertRaisesRegex(uu.Error, 'directory'):
158            uu.decode(io.BytesIO(relative_bad))
159        if os.altsep:
160            relative_bad_bs = relative_bad.replace(b'/', b'\\')
161            with self.assertRaisesRegex(uu.Error, 'directory'):
162                uu.decode(io.BytesIO(relative_bad_bs))
163
164        absolute_bad = b"""\
165begin 644 /tmp/test2
166$86)C"@``
167`
168end
169"""
170        with self.assertRaisesRegex(uu.Error, 'directory'):
171            uu.decode(io.BytesIO(absolute_bad))
172        if os.altsep:
173            absolute_bad_bs = absolute_bad.replace(b'/', b'\\')
174            with self.assertRaisesRegex(uu.Error, 'directory'):
175                uu.decode(io.BytesIO(absolute_bad_bs))
176
177
178class UUStdIOTest(unittest.TestCase):
179
180    def setUp(self):
181        self.stdin = sys.stdin
182        self.stdout = sys.stdout
183
184    def tearDown(self):
185        sys.stdin = self.stdin
186        sys.stdout = self.stdout
187
188    def test_encode(self):
189        sys.stdin = FakeIO(plaintext.decode("ascii"))
190        sys.stdout = FakeIO()
191        uu.encode("-", "-", "t1", 0o666)
192        self.assertEqual(sys.stdout.getvalue(),
193                         encodedtextwrapped(0o666, "t1").decode("ascii"))
194
195    def test_decode(self):
196        sys.stdin = FakeIO(encodedtextwrapped(0o666, "t1").decode("ascii"))
197        sys.stdout = FakeIO()
198        uu.decode("-", "-")
199        stdout = sys.stdout
200        sys.stdout = self.stdout
201        sys.stdin = self.stdin
202        self.assertEqual(stdout.getvalue(), plaintext.decode("ascii"))
203
204class UUFileTest(unittest.TestCase):
205
206    def setUp(self):
207        # uu.encode() supports only ASCII file names
208        self.tmpin  = os_helper.TESTFN_ASCII + "i"
209        self.tmpout = os_helper.TESTFN_ASCII + "o"
210        self.addCleanup(os_helper.unlink, self.tmpin)
211        self.addCleanup(os_helper.unlink, self.tmpout)
212
213    def test_encode(self):
214        with open(self.tmpin, 'wb') as fin:
215            fin.write(plaintext)
216
217        with open(self.tmpin, 'rb') as fin:
218            with open(self.tmpout, 'wb') as fout:
219                uu.encode(fin, fout, self.tmpin, mode=0o644)
220
221        with open(self.tmpout, 'rb') as fout:
222            s = fout.read()
223        self.assertEqual(s, encodedtextwrapped(0o644, self.tmpin))
224
225        # in_file and out_file as filenames
226        uu.encode(self.tmpin, self.tmpout, self.tmpin, mode=0o644)
227        with open(self.tmpout, 'rb') as fout:
228            s = fout.read()
229        self.assertEqual(s, encodedtextwrapped(0o644, self.tmpin))
230
231    # decode() calls chmod()
232    @os_helper.skip_unless_working_chmod
233    def test_decode(self):
234        with open(self.tmpin, 'wb') as f:
235            f.write(encodedtextwrapped(0o644, self.tmpout))
236
237        with open(self.tmpin, 'rb') as f:
238            uu.decode(f)
239
240        with open(self.tmpout, 'rb') as f:
241            s = f.read()
242        self.assertEqual(s, plaintext)
243        # XXX is there an xp way to verify the mode?
244
245    @os_helper.skip_unless_working_chmod
246    def test_decode_filename(self):
247        with open(self.tmpin, 'wb') as f:
248            f.write(encodedtextwrapped(0o644, self.tmpout))
249
250        uu.decode(self.tmpin)
251
252        with open(self.tmpout, 'rb') as f:
253            s = f.read()
254        self.assertEqual(s, plaintext)
255
256    @os_helper.skip_unless_working_chmod
257    def test_decodetwice(self):
258        # Verify that decode() will refuse to overwrite an existing file
259        with open(self.tmpin, 'wb') as f:
260            f.write(encodedtextwrapped(0o644, self.tmpout))
261        with open(self.tmpin, 'rb') as f:
262            uu.decode(f)
263
264        with open(self.tmpin, 'rb') as f:
265            self.assertRaises(uu.Error, uu.decode, f)
266
267    @os_helper.skip_unless_working_chmod
268    def test_decode_mode(self):
269        # Verify that decode() will set the given mode for the out_file
270        expected_mode = 0o444
271        with open(self.tmpin, 'wb') as f:
272            f.write(encodedtextwrapped(expected_mode, self.tmpout))
273
274        # make file writable again, so it can be removed (Windows only)
275        self.addCleanup(os.chmod, self.tmpout, expected_mode | stat.S_IWRITE)
276
277        with open(self.tmpin, 'rb') as f:
278            uu.decode(f)
279
280        self.assertEqual(
281            stat.S_IMODE(os.stat(self.tmpout).st_mode),
282            expected_mode
283        )
284
285
286if __name__=="__main__":
287    unittest.main()
288