Skip to content

Commit 87aa7dc

Browse files
Issue #17812: Fixed quadratic complexity of base64.b32encode().
Optimize base64.b32encode() and base64.b32decode() (speed up to 3x).
2 parents fef34e3 + 2c3f2f1 commit 87aa7dc

2 files changed

Lines changed: 51 additions & 77 deletions

File tree

Lib/base64.py

Lines changed: 48 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -138,21 +138,10 @@ def urlsafe_b64decode(s):
138138

139139

140140
# Base32 encoding/decoding must be done in Python
141-
_b32alphabet = {
142-
0: b'A', 9: b'J', 18: b'S', 27: b'3',
143-
1: b'B', 10: b'K', 19: b'T', 28: b'4',
144-
2: b'C', 11: b'L', 20: b'U', 29: b'5',
145-
3: b'D', 12: b'M', 21: b'V', 30: b'6',
146-
4: b'E', 13: b'N', 22: b'W', 31: b'7',
147-
5: b'F', 14: b'O', 23: b'X',
148-
6: b'G', 15: b'P', 24: b'Y',
149-
7: b'H', 16: b'Q', 25: b'Z',
150-
8: b'I', 17: b'R', 26: b'2',
151-
}
152-
153-
_b32tab = [v[0] for k, v in sorted(_b32alphabet.items())]
154-
_b32rev = dict([(v[0], k) for k, v in _b32alphabet.items()])
155-
141+
_b32alphabet = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
142+
_b32tab = [bytes([i]) for i in _b32alphabet]
143+
_b32tab2 = [a + b for a in _b32tab for b in _b32tab]
144+
_b32rev = {v: k for k, v in enumerate(_b32alphabet)}
156145

157146
def b32encode(s):
158147
"""Encode a byte string using Base32.
@@ -161,41 +150,30 @@ def b32encode(s):
161150
"""
162151
if not isinstance(s, bytes_types):
163152
raise TypeError("expected bytes, not %s" % s.__class__.__name__)
164-
quanta, leftover = divmod(len(s), 5)
153+
leftover = len(s) % 5
165154
# Pad the last quantum with zero bits if necessary
166155
if leftover:
167156
s = s + bytes(5 - leftover) # Don't use += !
168-
quanta += 1
169-
encoded = bytes()
170-
for i in range(quanta):
171-
# c1 and c2 are 16 bits wide, c3 is 8 bits wide. The intent of this
172-
# code is to process the 40 bits in units of 5 bits. So we take the 1
173-
# leftover bit of c1 and tack it onto c2. Then we take the 2 leftover
174-
# bits of c2 and tack them onto c3. The shifts and masks are intended
175-
# to give us values of exactly 5 bits in width.
176-
c1, c2, c3 = struct.unpack('!HHB', s[i*5:(i+1)*5])
177-
c2 += (c1 & 1) << 16 # 17 bits wide
178-
c3 += (c2 & 3) << 8 # 10 bits wide
179-
encoded += bytes([_b32tab[c1 >> 11], # bits 1 - 5
180-
_b32tab[(c1 >> 6) & 0x1f], # bits 6 - 10
181-
_b32tab[(c1 >> 1) & 0x1f], # bits 11 - 15
182-
_b32tab[c2 >> 12], # bits 16 - 20 (1 - 5)
183-
_b32tab[(c2 >> 7) & 0x1f], # bits 21 - 25 (6 - 10)
184-
_b32tab[(c2 >> 2) & 0x1f], # bits 26 - 30 (11 - 15)
185-
_b32tab[c3 >> 5], # bits 31 - 35 (1 - 5)
186-
_b32tab[c3 & 0x1f], # bits 36 - 40 (1 - 5)
187-
])
157+
encoded = bytearray()
158+
from_bytes = int.from_bytes
159+
b32tab2 = _b32tab2
160+
for i in range(0, len(s), 5):
161+
c = from_bytes(s[i: i + 5], 'big')
162+
encoded += (b32tab2[c >> 30] + # bits 1 - 10
163+
b32tab2[(c >> 20) & 0x3ff] + # bits 11 - 20
164+
b32tab2[(c >> 10) & 0x3ff] + # bits 21 - 30
165+
b32tab2[c & 0x3ff] # bits 31 - 40
166+
)
188167
# Adjust for any leftover partial quanta
189168
if leftover == 1:
190-
return encoded[:-6] + b'======'
169+
encoded[-6:] = b'======'
191170
elif leftover == 2:
192-
return encoded[:-4] + b'===='
171+
encoded[-4:] = b'===='
193172
elif leftover == 3:
194-
return encoded[:-3] + b'==='
173+
encoded[-3:] = b'==='
195174
elif leftover == 4:
196-
return encoded[:-1] + b'='
197-
return encoded
198-
175+
encoded[-1:] = b'='
176+
return bytes(encoded)
199177

200178
def b32decode(s, casefold=False, map01=None):
201179
"""Decode a Base32 encoded byte string.
@@ -217,8 +195,7 @@ def b32decode(s, casefold=False, map01=None):
217195
characters present in the input.
218196
"""
219197
s = _bytes_from_decode_data(s)
220-
quanta, leftover = divmod(len(s), 8)
221-
if leftover:
198+
if len(s) % 8:
222199
raise binascii.Error('Incorrect padding')
223200
# Handle section 2.4 zero and one mapping. The flag map01 will be either
224201
# False, or the character to map the digit 1 (one) to. It should be
@@ -232,42 +209,36 @@ def b32decode(s, casefold=False, map01=None):
232209
# Strip off pad characters from the right. We need to count the pad
233210
# characters because this will tell us how many null bytes to remove from
234211
# the end of the decoded string.
235-
padchars = 0
236-
mo = re.search(b'(?P<pad>[=]*)$', s)
237-
if mo:
238-
padchars = len(mo.group('pad'))
239-
if padchars > 0:
240-
s = s[:-padchars]
212+
l = len(s)
213+
s = s.rstrip(b'=')
214+
padchars = l - len(s)
241215
# Now decode the full quanta
242-
parts = []
243-
acc = 0
244-
shift = 35
245-
for c in s:
246-
val = _b32rev.get(c)
247-
if val is None:
216+
decoded = bytearray()
217+
b32rev = _b32rev
218+
for i in range(0, len(s), 8):
219+
quanta = s[i: i + 8]
220+
acc = 0
221+
try:
222+
for c in quanta:
223+
acc = (acc << 5) + b32rev[c]
224+
except KeyError:
248225
raise TypeError('Non-base32 digit found')
249-
acc += _b32rev[c] << shift
250-
shift -= 5
251-
if shift < 0:
252-
parts.append(binascii.unhexlify(bytes('%010x' % acc, "ascii")))
253-
acc = 0
254-
shift = 35
226+
decoded += acc.to_bytes(5, 'big')
255227
# Process the last, partial quanta
256-
last = binascii.unhexlify(bytes('%010x' % acc, "ascii"))
257-
if padchars == 0:
258-
last = b'' # No characters
259-
elif padchars == 1:
260-
last = last[:-1]
261-
elif padchars == 3:
262-
last = last[:-2]
263-
elif padchars == 4:
264-
last = last[:-3]
265-
elif padchars == 6:
266-
last = last[:-4]
267-
else:
268-
raise binascii.Error('Incorrect padding')
269-
parts.append(last)
270-
return b''.join(parts)
228+
if padchars:
229+
acc <<= 5 * padchars
230+
last = acc.to_bytes(5, 'big')
231+
if padchars == 1:
232+
decoded[-5:] = last[:-1]
233+
elif padchars == 3:
234+
decoded[-5:] = last[:-2]
235+
elif padchars == 4:
236+
decoded[-5:] = last[:-3]
237+
elif padchars == 6:
238+
decoded[-5:] = last[:-4]
239+
else:
240+
raise binascii.Error('Incorrect padding')
241+
return bytes(decoded)
271242

272243

273244

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ What's New in Python 3.4.0 Alpha 1?
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #17812: Fixed quadratic complexity of base64.b32encode().
14+
Optimize base64.b32encode() and base64.b32decode() (speed up to 3x).
15+
1316
- Issue #17937: Try harder to collect cyclic garbage at shutdown.
1417

1518
- Issue #12370: Prevent class bodies from interfering with the __class__

0 commit comments

Comments
 (0)