8000 bpo-31116: Add Z85 variant to base64 (GH-30598) · python/cpython@c40b5b9 · GitHub
[go: up one dir, main page]

Skip to content

Commit c40b5b9

Browse files
authored
bpo-31116: Add Z85 variant to base64 (GH-30598)
Z85 specification: https://rfc.zeromq.org/spec/32/
1 parent 9402ea6 commit c40b5b9

File tree

5 files changed

+141
-2
lines changed

5 files changed

+141
-2
lines changed

Doc/library/base64.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,24 @@ The modern interface provides:
244244
.. versionadded:: 3.4
245245

246246

247+
.. function:: z85encode(s)
248+
249+
Encode the :term:`bytes-like object` *s* using Z85 (as used in ZeroMQ)
250+
and return the encoded :class:`bytes`. See `Z85 specification
251+
<https://rfc.zeromq.org/spec/32/>`_ for more information.
252+
253+
.. versionadded:: 3.13
254+
255+
256+
.. function:: z85decode(s)
257+
258+
Decode the Z85-encoded :term:`bytes-like object` or ASCII string *s* and
259+
return the decoded :class:`bytes`. See `Z85 specification
260+
<https://rfc.zeromq.org/spec/32/>`_ for more information.
261+
262+
.. versionadded:: 3.13
263+
264+
247265
The legacy interface:
248266

249267
.. function:: decode(input, output)

Doc/whatsnew/3.13.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ asyncio
224224
the buffer size.
225225
(Contributed by Jamie Phan in :gh:`115199`.)
226226

227+
base64
228+
---
229+
230+
* Add :func:`base64.z85encode` and :func:`base64.z85decode` functions which allow encoding
231+
and decoding z85 data.
232+
See `Z85 specification <https://rfc.zeromq.org/spec/32/>`_ for more information.
233+
(Contributed by Matan Perelman in :gh:`75299`.)
234+
227235
copy
228236
----
229237

Lib/base64.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
'b64encode', 'b64decode', 'b32encode', 'b32decode',
1919
'b32hexencode', 'b32hexdecode', 'b16encode', 'b16decode',
2020
# Base85 and Ascii85 encodings
21-
'b85encode', 'b85decode', 'a85encode', 'a85decode',
21+
'b85encode', 'b85decode', 'a85encode', 'a85decode', 'z85encode', 'z85decode',
2222
# Standard Base64 encoding
2323
'standard_b64encode', 'standard_b64decode',
2424
# Some common Base64 alternatives. As referenced by RFC 3458, see thread
@@ -497,6 +497,33 @@ def b85decode(b):
497497
result = result[:-padding]
498498
return result
499499

500+
_z85alphabet = (b'0123456789abcdefghijklmnopqrstuvwxyz'
501+
b'ABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#')
502+
# Translating b85 valid but z85 invalid chars to b'\x00' is required
503+
# to prevent them from being decoded as b85 valid chars.
504+
_z85_b85_decode_diff = b';_`|~'
505+
_z85_decode_translation = bytes.maketrans(
506+
_z85alphabet + _z85_b85_decode_diff,
507+
_b85alphabet + b'\x00' * len(_z85_b85_decode_diff)
508+
)
509+
_z85_encode_translation = bytes.maketrans(_b85alphabet, _z85alphabet)
510+
511+
def z85encode(s):
512+
"""Encode bytes-like object b in z85 format and return a bytes object."""
513+
return b85encode(s).translate(_z85_encode_translation)
514+
515+
def z85decode(s):
516+
"""Decode the z85-encoded bytes-like object or ASCII string b
517+
518+
The result is returned as a bytes object.
519+
"""
520+
s = _bytes_from_decode_data(s)
521+
s = s.translate(_z85_decode_translation)
522+
try:
523+
return b85decode(s)
524+
except ValueError as e:
525+
raise ValueError(e.args[0].replace('base85', 'z85')) from None
526+
500527
# Legacy interface. This code could be cleaned up since I don't believe
501528
# binascii has any line length limitations. It just doesn't seem worth it
502529
# though. The files should be opened in binary mode.

Lib/test/test_base64.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,40 @@ def test_b85encode(self):
545545
self.check_other_types(base64.b85encode, b"www.python.org",
546546
b'cXxL#aCvlSZ*DGca%T')
547547

548+
def test_z85encode(self):
549+
eq = self.assertEqual
550+
551+
tests = {
552+
b'': b'',
553+
b'www.python.org': b'CxXl-AcVLsz/dgCA+t',
554+
bytes(range(255)): b"""009c61o!#m2NH?C3>iWS5d]J*6CRx17-skh9337x"""
555+
b"""ar.{NbQB=+c[cR@eg&FcfFLssg=mfIi5%2YjuU>)kTv.7l}6Nnnj=AD"""
556+
b"""oIFnTp/ga?r8($2sxO*itWpVyu$0IOwmYv=xLzi%y&a6dAb/]tBAI+J"""
557+
b"""CZjQZE0{D[FpSr8GOteoH(41EJe-<UKDCY&L:dM3N3<zjOsMmzPRn9P"""
558+
b"""Q[%@^ShV!$TGwUeU^7HuW6^uKXvGh.YUh4]Z})[9-kP:p:JqPF+*1CV"""
559+
b"""^9Zp<!yAd4/Xb0k*$*&A&nJXQ<MkK!>&}x#)cTlf[Bu8v].4}L}1:^-"""
560+
b"""@qDP""",
561+
b"""abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"""
562+
b"""0123456789!@#0^&*();:<>,. []{}""":
563+
b"""vpA.SwObN*x>?B1zeKohADlbxB-}$ND3R+ylQTvjm[uizoh55PpF:[^"""
564+
b"""q=D:$s6eQefFLssg=mfIi5@cEbqrBJdKV-ciY]OSe*aw7DWL""",
565+
b'no padding..': b'zF{UpvpS[.zF7NO',
566+
b'zero compression\x00\x00\x00\x00': b'Ds.bnay/tbAb]JhB7]Mg00000',
567+
b'zero compression\x00\x00\x00': b'Ds.bnay/tbAb]JhB7]Mg0000',
568+
b"""Boundary:\x00\x00\x00\x00""": b"""lt}0:wmoI7iSGcW00""",
569+
b'Space compr: ': b'q/DePwGUG3ze:IRarR^H',
570+
b'\xff': b'@@',
571+
b'\xff'*2: b'%nJ',
572+
b'\xff'*3: b'%nS9',
573+
b'\xff'*4: b'%nSc0',
574+
}
575+
576+
for data, res in tests.items():
577+
eq(base64.z85encode(data), res)
578+
579+
self.check_other_types(base64.z85encode, b"www.python.org",
580+
b'CxXl-AcVLsz/dgCA+t')
581+
548582
def test_a85decode(self):
549583
eq = self.assertEqual
550584

@@ -626,6 +660,41 @@ def test_b85decode(self):
626660
self.check_other_types(base64.b85decode, b'cXxL#aCvlSZ*DGca%T',
627661
b"www.python.org")
628662

663+
def test_z85decode(self):
664+
eq = self.assertEqual
665+
666+
tests = {
667+
b'': b'',
668+
b'CxXl-AcVLsz/dgCA+t': b'www.python.org',
669+
b"""009c61o!#m2NH?C3>iWS5d]J*6CRx17-skh9337x"""
670+
b"""ar.{NbQB=+c[cR@eg&FcfFLssg=mfIi5%2YjuU>)kTv.7l}6Nnnj=AD"""
671+
b"""oIFnTp/ga?r8($2sxO*itWpVyu$0IOwmYv=xLzi%y&a6dAb/]tBAI+J"""
672+
b"""CZjQZE0{D[FpSr8GOteoH(41EJe-<UKDCY&L:dM3N3<zjOsMmzPRn9P"""
673+
b"""Q[%@^ShV!$TGwUeU^7HuW6^uKXvGh.YUh4]Z})[9-kP:p:JqPF+*1CV"""
674+
b"""^9Zp<!yAd4/Xb0k*$*&A&nJXQ<MkK!>&}x#)cTlf[Bu8v].4}L}1:^-"""
675+
b"""@qDP""": bytes(range(255)),
676+
b"""vpA.SwObN*x>?B1zeKohADlbxB-}$ND3R+ylQTvjm[uizoh55PpF:[^"""
677+
b"""q=D:$s6eQefFLssg=mfIi5@cEbqrBJdKV-ciY]OSe*aw7DWL""":
678+
b"""abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"""
679+
b"""0123456789!@#0^&*();:<>,. []{}""",
680+
b'zF{UpvpS[.zF7NO': b'no padding..',
681+
b'Ds.bnay/tbAb]JhB7]Mg00000': b'zero compression\x00\x00\x00\x00',
682+
b'Ds.bnay/tbAb]JhB7]Mg0000': b'zero compression\x00\x00\x00',
683+
b"""lt}0:wmoI7iSGcW00""": b"""Boundary:\x00\x00\x00\x00""",
684+
b'q/DePwGUG3ze:IRarR^H': b'Space compr: ',
685+
b'@@': b'\xff',
686+
b'%nJ': b'\xff'*2,
687+
b'%nS9': b'\xff'*3,
688+
b'%nSc0': b'\xff'*4,
689+
}
690+
691+
for data, res in tests.items():
692+
eq(base64.z85decode(data), res)
693+
eq(base64.z85decode(data.decode("ascii")), res)
694+
695+
self.check_other_types(base64.z85decode, b'CxXl-AcVLsz/dgCA+t',
696+
b'www.python.org')
697+
629698
def test_a85_padding(self):
630699
eq = self.assertEqual
631700

@@ -707,14 +776,30 @@ def test_b85decode_errors(self):
707776
self.assertRaises(ValueError, base64.b85decode, b'|NsC')
708777
self.assertRaises(ValueError, base64.b85decode, b'|NsC1')
709778

779+
def test_z85decode_errors(self):
780+
illegal = list(range(33)) + \
781+
list(b'"\',;_`|\\~') + \
782+
list(range(128, 256))
783+
for c in illegal:
784+
with self.assertRaises(ValueError, msg=bytes([c])):
785+
base64.z85decode(b'0000' + bytes([c]))
786+
787+
# b'\xff\xff\xff\xff' encodes to b'%nSc0', the following will overflow:
788+
self.assertRaises(ValueError, base64.z85decode, b'%')
789+
self.assertRaises(ValueError, base64.z85decode, b'%n')
790+
self.assertRaises(ValueError, base64.z85decode, b'%nS')
791+
self.assertRaises(ValueError, base64.z85decode, b'%nSc')
792+
self.assertRaises(ValueError, base64.z85decode, b'%nSc1')
793+
710794
def test_decode_nonascii_str(self):
711795
decode_funcs = (base64.b64decode,
712796
base64.standard_b64decode,
713797
base64.urlsafe_b64decode,
714798
base64.b32decode,
715799
base64.b16decode,
716800
base64.b85decode,
717-
base64.a85decode)
801+
base64.a85decode,
802+
base64.z85decode)
718803
for f in decode_funcs:
719804
self.assertRaises(ValueError, f, 'with non-ascii \xcb')
720805

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add Z85 encoding to ``base64``.

0 commit comments

Comments
 (0)
0