Repository: liberapay/liberapay.com

Issue:
b64decode_s with unicode crashes.
Was working on https://github.com/gratipay/gratipay.com/pull/3965 when I ran into this.
According to http://stackoverflow.com/a/2230623/2643846

> b64decode quite explicitly can only take bytes (a string), not unicode.

Steps to reproduce - 
1. `b64encode_s('TheEnter?prise')` => `VGhlRW50ZXI_cHJpc2U~`
2. `b64decode_s('VGhlRW50ZXI_cHJpc2U~')` => `TypeError: character mapping must return integer, None or unicode` (deep down in the stack trace)

b64decode_s with unicode crashes.
Was working on https://github.com/gratipay/gratipay.com/pull/3965 when I ran into this.
According to http://stackoverflow.com/a/2230623/2643846

> b64decode quite explicitly can only take bytes (a string), not unicode.

Steps to reproduce - 
1. `b64encode_s('TheEnter?prise')` => `VGhlRW50ZXI_cHJpc2U~`
2. `b64decode_s('VGhlRW50ZXI_cHJpc2U~')` => `TypeError: character mapping must return integer, None or unicode` (deep down in the stack trace)



Discussion:

Resolving patch:
diff --git a/liberapay/utils/__init__.py b/liberapay/utils/__init__.py
--- a/liberapay/utils/__init__.py
+++ b/liberapay/utils/__init__.py
@@ -108,6 +108,16 @@ def get_community(state, restrict=False):
 
 
 def b64decode_s(s, **kw):
+    def error():
+        if 'default' in kw:
+            return kw['default']
+        raise Response(400, "invalid base64 input")
+
+    try:
+        s = s.encode('ascii')
+    except UnicodeError:
+        return error()
+
     udecode = lambda a: a.decode('utf8')
     if s[:1] == b'.':
         udecode = lambda a: a
@@ -121,9 +131,7 @@ def b64decode_s(s, **kw):
             return udecode(b64decode(s))
         except Exception:
             pass
-        if 'default' in kw:
-            return kw['default']
-        raise Response(400, "invalid base64 input")
+        return error()
 
 
 def b64encode_s(s):


Test patch:
diff --git a/tests/py/test_utils.py b/tests/py/test_utils.py
--- a/tests/py/test_utils.py
+++ b/tests/py/test_utils.py
@@ -6,7 +6,7 @@
 from aspen.http.response import Response
 from liberapay import utils
 from liberapay.testing import Harness
-from liberapay.utils import i18n, markdown
+from liberapay.utils import i18n, markdown, b64encode_s, b64decode_s
 
 
 class Tests(Harness):
@@ -119,5 +119,27 @@ def test_markdown_render_no_intra_emphasis(self):
         assert expected == actual
 
 
+    # Base64 encoding/decoding
+    # ========================
+
     def test_safe_base64_transcode_works_with_binary_data(self):
         utils.b64decode_s(utils.b64encode_s(b'\xff'))
+
+    def test_b64encode_s_replaces_slash_with_underscore(self):
+        # TheEnter?prise => VGhlRW50ZXI/cHJpc2U=
+        assert b64encode_s('TheEnter?prise') == 'VGhlRW50ZXI_cHJpc2U~'
+
+    def test_b64encode_s_replaces_equals_with_tilde(self):
+        assert b64encode_s('TheEnterprise') == 'VGhlRW50ZXJwcmlzZQ~~'
+
+    def test_b64decode_s_decodes(self):
+        assert b64decode_s('VGhlRW50ZXI_cHJpc2U~') == 'TheEnter?prise'
+
+    def test_b64decode_s_raises_response_on_error(self):
+        with self.assertRaises(Response) as cm:
+            b64decode_s('abcd')
+        assert cm.exception.code == 400
+
+    def test_b64decode_s_returns_default_if_passed_on_error(self):
+        assert b64decode_s('abcd', default='error') == 'error'
+