bpo-30103: Allow Uuencode in Python using backtick as zero instead of space (#1326)


diff --git a/Lib/test/test_binascii.py b/Lib/test/test_binascii.py
index 6b3e437..8fa57cd 100644
--- a/Lib/test/test_binascii.py
+++ b/Lib/test/test_binascii.py
@@ -112,29 +112,41 @@
 
     def test_uu(self):
         MAX_UU = 45
-        lines = []
-        for i in range(0, len(self.data), MAX_UU):
-            b = self.type2test(self.rawdata[i:i+MAX_UU])
-            a = binascii.b2a_uu(b)
-            lines.append(a)
-        res = bytes()
-        for line in lines:
-            a = self.type2test(line)
-            b = binascii.a2b_uu(a)
-            res += b
-        self.assertEqual(res, self.rawdata)
+        for backtick in (True, False):
+            lines = []
+            for i in range(0, len(self.data), MAX_UU):
+                b = self.type2test(self.rawdata[i:i+MAX_UU])
+                a = binascii.b2a_uu(b, backtick=backtick)
+                lines.append(a)
+            res = bytes()
+            for line in lines:
+                a = self.type2test(line)
+                b = binascii.a2b_uu(a)
+                res += b
+            self.assertEqual(res, self.rawdata)
 
         self.assertEqual(binascii.a2b_uu(b"\x7f"), b"\x00"*31)
         self.assertEqual(binascii.a2b_uu(b"\x80"), b"\x00"*32)
         self.assertEqual(binascii.a2b_uu(b"\xff"), b"\x00"*31)
         self.assertRaises(binascii.Error, binascii.a2b_uu, b"\xff\x00")
         self.assertRaises(binascii.Error, binascii.a2b_uu, b"!!!!")
-
         self.assertRaises(binascii.Error, binascii.b2a_uu, 46*b"!")
 
         # Issue #7701 (crash on a pydebug build)
         self.assertEqual(binascii.b2a_uu(b'x'), b'!>   \n')
 
+        self.assertEqual(binascii.b2a_uu(b''), b' \n')
+        self.assertEqual(binascii.b2a_uu(b'', backtick=True), b'`\n')
+        self.assertEqual(binascii.a2b_uu(b' \n'), b'')
+        self.assertEqual(binascii.a2b_uu(b'`\n'), b'')
+        self.assertEqual(binascii.b2a_uu(b'\x00Cat'), b'$ $-A=   \n')
+        self.assertEqual(binascii.b2a_uu(b'\x00Cat', backtick=True),
+                         b'$`$-A=```\n')
+        self.assertEqual(binascii.a2b_uu(b'$`$-A=```\n'),
+                         binascii.a2b_uu(b'$ $-A=   \n'))
+        with self.assertRaises(TypeError):
+            binascii.b2a_uu(b"", True)
+
     def test_crc_hqx(self):
         crc = binascii.crc_hqx(self.type2test(b"Test the CRC-32 of"), 0)
         crc = binascii.crc_hqx(self.type2test(b" this string."), crc)
diff --git a/Lib/test/test_uu.py b/Lib/test/test_uu.py
index ad2f2c5..11bd08c 100644
--- a/Lib/test/test_uu.py
+++ b/Lib/test/test_uu.py
@@ -10,11 +10,11 @@
 import uu
 import io
 
-plaintext = b"The smooth-scaled python crept over the sleeping dog\n"
+plaintext = b"The symbols on top of your keyboard are !@#$%^&*()_+|~\n"
 
 encodedtext = b"""\
-M5&AE('-M;V]T:\"US8V%L960@<'ET:&]N(&-R97!T(&]V97(@=&AE('-L965P
-(:6YG(&1O9PH """
+M5&AE('-Y;6)O;',@;VX@=&]P(&]F('EO=7(@:V5Y8F]A<F0@87)E("% (R0E
+*7B8J*"E?*WQ^"@  """
 
 # Stolen from io.py
 class FakeIO(io.TextIOWrapper):
@@ -44,9 +44,14 @@
         return self.buffer.getvalue().decode(self._encoding, self._errors)
 
 
-def encodedtextwrapped(mode, filename):
-    return (bytes("begin %03o %s\n" % (mode, filename), "ascii") +
-            encodedtext + b"\n \nend\n")
+def encodedtextwrapped(mode, filename, backtick=False):
+    if backtick:
+        res = (bytes("begin %03o %s\n" % (mode, filename), "ascii") +
+               encodedtext.replace(b' ', b'`') + b"\n`\nend\n")
+    else:
+        res = (bytes("begin %03o %s\n" % (mode, filename), "ascii") +
+               encodedtext + b"\n \nend\n")
+    return res
 
 class UUTest(unittest.TestCase):
 
@@ -59,20 +64,27 @@
         out = io.BytesIO()
         uu.encode(inp, out, "t1", 0o644)
         self.assertEqual(out.getvalue(), encodedtextwrapped(0o644, "t1"))
+        inp = io.BytesIO(plaintext)
+        out = io.BytesIO()
+        uu.encode(inp, out, "t1", backtick=True)
+        self.assertEqual(out.getvalue(), encodedtextwrapped(0o666, "t1", True))
+        with self.assertRaises(TypeError):
+            uu.encode(inp, out, "t1", 0o644, True)
 
     def test_decode(self):
-        inp = io.BytesIO(encodedtextwrapped(0o666, "t1"))
-        out = io.BytesIO()
-        uu.decode(inp, out)
-        self.assertEqual(out.getvalue(), plaintext)
-        inp = io.BytesIO(
-            b"UUencoded files may contain many lines,\n" +
-            b"even some that have 'begin' in them.\n" +
-            encodedtextwrapped(0o666, "t1")
-        )
-        out = io.BytesIO()
-        uu.decode(inp, out)
-        self.assertEqual(out.getvalue(), plaintext)
+        for backtick in True, False:
+            inp = io.BytesIO(encodedtextwrapped(0o666, "t1", backtick=backtick))
+            out = io.BytesIO()
+            uu.decode(inp, out)
+            self.assertEqual(out.getvalue(), plaintext)
+            inp = io.BytesIO(
+                b"UUencoded files may contain many lines,\n" +
+                b"even some that have 'begin' in them.\n" +
+                encodedtextwrapped(0o666, "t1", backtick=backtick)
+            )
+            out = io.BytesIO()
+            uu.decode(inp, out)
+            self.assertEqual(out.getvalue(), plaintext)
 
     def test_truncatedinput(self):
         inp = io.BytesIO(b"begin 644 t1\n" + encodedtext)
@@ -94,25 +106,33 @@
 
     def test_garbage_padding(self):
         # Issue #22406
-        encodedtext = (
+        encodedtext1 = (
             b"begin 644 file\n"
             # length 1; bits 001100 111111 111111 111111
             b"\x21\x2C\x5F\x5F\x5F\n"
             b"\x20\n"
             b"end\n"
         )
+        encodedtext2 = (
+            b"begin 644 file\n"
+            # length 1; bits 001100 111111 111111 111111
+            b"\x21\x2C\x5F\x5F\x5F\n"
+            b"\x60\n"
+            b"end\n"
+        )
         plaintext = b"\x33"  # 00110011
 
-        with self.subTest("uu.decode()"):
-            inp = io.BytesIO(encodedtext)
-            out = io.BytesIO()
-            uu.decode(inp, out, quiet=True)
-            self.assertEqual(out.getvalue(), plaintext)
+        for encodedtext in encodedtext1, encodedtext2:
+            with self.subTest("uu.decode()"):
+                inp = io.BytesIO(encodedtext)
+                out = io.BytesIO()
+                uu.decode(inp, out, quiet=True)
+                self.assertEqual(out.getvalue(), plaintext)
 
-        with self.subTest("uu_codec"):
-            import codecs
-            decoded = codecs.decode(encodedtext, "uu_codec")
-            self.assertEqual(decoded, plaintext)
+            with self.subTest("uu_codec"):
+                import codecs
+                decoded = codecs.decode(encodedtext, "uu_codec")
+                self.assertEqual(decoded, plaintext)
 
 class UUStdIOTest(unittest.TestCase):
 
@@ -250,11 +270,6 @@
         finally:
             self._kill(f)
 
-def test_main():
-    support.run_unittest(UUTest,
-                              UUStdIOTest,
-                              UUFileTest,
-                              )
 
 if __name__=="__main__":
-    test_main()
+    unittest.main()
diff --git a/Lib/uu.py b/Lib/uu.py
index d68d293..8333e86 100755
--- a/Lib/uu.py
+++ b/Lib/uu.py
@@ -26,8 +26,8 @@
 
 """Implementation of the UUencode and UUdecode functions.
 
-encode(in_file, out_file [,name, mode])
-decode(in_file [, out_file, mode])
+encode(in_file, out_file [,name, mode], *, backtick=False)
+decode(in_file [, out_file, mode, quiet])
 """
 
 import binascii
@@ -39,7 +39,7 @@
 class Error(Exception):
     pass
 
-def encode(in_file, out_file, name=None, mode=None):
+def encode(in_file, out_file, name=None, mode=None, *, backtick=False):
     """Uuencode file"""
     #
     # If in_file is a pathname open it and change defaults
@@ -79,9 +79,12 @@
         out_file.write(('begin %o %s\n' % ((mode & 0o777), name)).encode("ascii"))
         data = in_file.read(45)
         while len(data) > 0:
-            out_file.write(binascii.b2a_uu(data))
+            out_file.write(binascii.b2a_uu(data, backtick=backtick))
             data = in_file.read(45)
-        out_file.write(b' \nend\n')
+        if backtick:
+            out_file.write(b'`\nend\n')
+        else:
+            out_file.write(b' \nend\n')
     finally:
         for f in opened_files:
             f.close()