summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorMatan Perelman <matan1008@gmail.com>2024-02-25 17:17:54 (GMT)
committerGitHub <noreply@github.com>2024-02-25 17:17:54 (GMT)
commitc40b5b97fdac5099f670589b42c9038d7b23f2e5 (patch)
treed9eaf0d9c846588b08cef6d7ade487dfcc73208e /Lib
parent9402ea63f7897ecda7036f667949ad17f4208e5b (diff)
downloadcpython-c40b5b97fdac5099f670589b42c9038d7b23f2e5.zip
cpython-c40b5b97fdac5099f670589b42c9038d7b23f2e5.tar.gz
cpython-c40b5b97fdac5099f670589b42c9038d7b23f2e5.tar.bz2
bpo-31116: Add Z85 variant to base64 (GH-30598)
Z85 specification: https://rfc.zeromq.org/spec/32/
Diffstat (limited to 'Lib')
-rwxr-xr-xLib/base64.py29
-rw-r--r--Lib/test/test_base64.py87
2 files changed, 114 insertions, 2 deletions
diff --git a/Lib/base64.py b/Lib/base64.py
index e3e983b..25164d1 100755
--- a/Lib/base64.py
+++ b/Lib/base64.py
@@ -18,7 +18,7 @@ __all__ = [
'b64encode', 'b64decode', 'b32encode', 'b32decode',
'b32hexencode', 'b32hexdecode', 'b16encode', 'b16decode',
# Base85 and Ascii85 encodings
- 'b85encode', 'b85decode', 'a85encode', 'a85decode',
+ 'b85encode', 'b85decode', 'a85encode', 'a85decode', 'z85encode', 'z85decode',
# Standard Base64 encoding
'standard_b64encode', 'standard_b64decode',
# Some common Base64 alternatives. As referenced by RFC 3458, see thread
@@ -497,6 +497,33 @@ def b85decode(b):
result = result[:-padding]
return result
+_z85alphabet = (b'0123456789abcdefghijklmnopqrstuvwxyz'
+ b'ABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#')
+# Translating b85 valid but z85 invalid chars to b'\x00' is required
+# to prevent them from being decoded as b85 valid chars.
+_z85_b85_decode_diff = b';_`|~'
+_z85_decode_translation = bytes.maketrans(
+ _z85alphabet + _z85_b85_decode_diff,
+ _b85alphabet + b'\x00' * len(_z85_b85_decode_diff)
+)
+_z85_encode_translation = bytes.maketrans(_b85alphabet, _z85alphabet)
+
+def z85encode(s):
+ """Encode bytes-like object b in z85 format and return a bytes object."""
+ return b85encode(s).translate(_z85_encode_translation)
+
+def z85decode(s):
+ """Decode the z85-encoded bytes-like object or ASCII string b
+
+ The result is returned as a bytes object.
+ """
+ s = _bytes_from_decode_data(s)
+ s = s.translate(_z85_decode_translation)
+ try:
+ return b85decode(s)
+ except ValueError as e:
+ raise ValueError(e.args[0].replace('base85', 'z85')) from None
+
# Legacy interface. This code could be cleaned up since I don't believe
# binascii has any line length limitations. It just doesn't seem worth it
# though. The files should be opened in binary mode.
diff --git a/Lib/test/test_base64.py b/Lib/test/test_base64.py
index f6171d3..409c8c1 100644
--- a/Lib/test/test_base64.py
+++ b/Lib/test/test_base64.py
@@ -545,6 +545,40 @@ class BaseXYTestCase(unittest.TestCase):
self.check_other_types(base64.b85encode, b"www.python.org",
b'cXxL#aCvlSZ*DGca%T')
+ def test_z85encode(self):
+ eq = self.assertEqual
+
+ tests = {
+ b'': b'',
+ b'www.python.org': b'CxXl-AcVLsz/dgCA+t',
+ bytes(range(255)): b"""009c61o!#m2NH?C3>iWS5d]J*6CRx17-skh9337x"""
+ b"""ar.{NbQB=+c[cR@eg&FcfFLssg=mfIi5%2YjuU>)kTv.7l}6Nnnj=AD"""
+ b"""oIFnTp/ga?r8($2sxO*itWpVyu$0IOwmYv=xLzi%y&a6dAb/]tBAI+J"""
+ b"""CZjQZE0{D[FpSr8GOteoH(41EJe-<UKDCY&L:dM3N3<zjOsMmzPRn9P"""
+ b"""Q[%@^ShV!$TGwUeU^7HuW6^uKXvGh.YUh4]Z})[9-kP:p:JqPF+*1CV"""
+ b"""^9Zp<!yAd4/Xb0k*$*&A&nJXQ<MkK!>&}x#)cTlf[Bu8v].4}L}1:^-"""
+ b"""@qDP""",
+ b"""abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"""
+ b"""0123456789!@#0^&*();:<>,. []{}""":
+ b"""vpA.SwObN*x>?B1zeKohADlbxB-}$ND3R+ylQTvjm[uizoh55PpF:[^"""
+ b"""q=D:$s6eQefFLssg=mfIi5@cEbqrBJdKV-ciY]OSe*aw7DWL""",
+ b'no padding..': b'zF{UpvpS[.zF7NO',
+ b'zero compression\x00\x00\x00\x00': b'Ds.bnay/tbAb]JhB7]Mg00000',
+ b'zero compression\x00\x00\x00': b'Ds.bnay/tbAb]JhB7]Mg0000',
+ b"""Boundary:\x00\x00\x00\x00""": b"""lt}0:wmoI7iSGcW00""",
+ b'Space compr: ': b'q/DePwGUG3ze:IRarR^H',
+ b'\xff': b'@@',
+ b'\xff'*2: b'%nJ',
+ b'\xff'*3: b'%nS9',
+ b'\xff'*4: b'%nSc0',
+ }
+
+ for data, res in tests.items():
+ eq(base64.z85encode(data), res)
+
+ self.check_other_types(base64.z85encode, b"www.python.org",
+ b'CxXl-AcVLsz/dgCA+t')
+
def test_a85decode(self):
eq = self.assertEqual
@@ -626,6 +660,41 @@ class BaseXYTestCase(unittest.TestCase):
self.check_other_types(base64.b85decode, b'cXxL#aCvlSZ*DGca%T',
b"www.python.org")
+ def test_z85decode(self):
+ eq = self.assertEqual
+
+ tests = {
+ b'': b'',
+ b'CxXl-AcVLsz/dgCA+t': b'www.python.org',
+ b"""009c61o!#m2NH?C3>iWS5d]J*6CRx17-skh9337x"""
+ b"""ar.{NbQB=+c[cR@eg&FcfFLssg=mfIi5%2YjuU>)kTv.7l}6Nnnj=AD"""
+ b"""oIFnTp/ga?r8($2sxO*itWpVyu$0IOwmYv=xLzi%y&a6dAb/]tBAI+J"""
+ b"""CZjQZE0{D[FpSr8GOteoH(41EJe-<UKDCY&L:dM3N3<zjOsMmzPRn9P"""
+ b"""Q[%@^ShV!$TGwUeU^7HuW6^uKXvGh.YUh4]Z})[9-kP:p:JqPF+*1CV"""
+ b"""^9Zp<!yAd4/Xb0k*$*&A&nJXQ<MkK!>&}x#)cTlf[Bu8v].4}L}1:^-"""
+ b"""@qDP""": bytes(range(255)),
+ b"""vpA.SwObN*x>?B1zeKohADlbxB-}$ND3R+ylQTvjm[uizoh55PpF:[^"""
+ b"""q=D:$s6eQefFLssg=mfIi5@cEbqrBJdKV-ciY]OSe*aw7DWL""":
+ b"""abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"""
+ b"""0123456789!@#0^&*();:<>,. []{}""",
+ b'zF{UpvpS[.zF7NO': b'no padding..',
+ b'Ds.bnay/tbAb]JhB7]Mg00000': b'zero compression\x00\x00\x00\x00',
+ b'Ds.bnay/tbAb]JhB7]Mg0000': b'zero compression\x00\x00\x00',
+ b"""lt}0:wmoI7iSGcW00""": b"""Boundary:\x00\x00\x00\x00""",
+ b'q/DePwGUG3ze:IRarR^H': b'Space compr: ',
+ b'@@': b'\xff',
+ b'%nJ': b'\xff'*2,
+ b'%nS9': b'\xff'*3,
+ b'%nSc0': b'\xff'*4,
+ }
+
+ for data, res in tests.items():
+ eq(base64.z85decode(data), res)
+ eq(base64.z85decode(data.decode("ascii")), res)
+
+ self.check_other_types(base64.z85decode, b'CxXl-AcVLsz/dgCA+t',
+ b'www.python.org')
+
def test_a85_padding(self):
eq = self.assertEqual
@@ -707,6 +776,21 @@ class BaseXYTestCase(unittest.TestCase):
self.assertRaises(ValueError, base64.b85decode, b'|NsC')
self.assertRaises(ValueError, base64.b85decode, b'|NsC1')
+ def test_z85decode_errors(self):
+ illegal = list(range(33)) + \
+ list(b'"\',;_`|\\~') + \
+ list(range(128, 256))
+ for c in illegal:
+ with self.assertRaises(ValueError, msg=bytes([c])):
+ base64.z85decode(b'0000' + bytes([c]))
+
+ # b'\xff\xff\xff\xff' encodes to b'%nSc0', the following will overflow:
+ self.assertRaises(ValueError, base64.z85decode, b'%')
+ self.assertRaises(ValueError, base64.z85decode, b'%n')
+ self.assertRaises(ValueError, base64.z85decode, b'%nS')
+ self.assertRaises(ValueError, base64.z85decode, b'%nSc')
+ self.assertRaises(ValueError, base64.z85decode, b'%nSc1')
+
def test_decode_nonascii_str(self):
decode_funcs = (base64.b64decode,
base64.standard_b64decode,
@@ -714,7 +798,8 @@ class BaseXYTestCase(unittest.TestCase):
base64.b32decode,
base64.b16decode,
base64.b85decode,
- base64.a85decode)
+ base64.a85decode,
+ base64.z85decode)
for f in decode_funcs:
self.assertRaises(ValueError, f, 'with non-ascii \xcb')