Skip to content

Commit eb03366

Browse files
Michael Englodhermes
authored andcommitted
Strip base64 padding characters from urlsafe in Datastore's (to|from)_legacy_urlsafe (googleapis#3560)
Also * add padding characters in `from_legacy_urlsafe` if needed * add an extra example in the unit tests that actually requires base64 padding
1 parent 8ab1afe commit eb03366

2 files changed

Lines changed: 40 additions & 14 deletions

File tree

datastore/google/cloud/datastore/key.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ def to_legacy_urlsafe(self):
304304
This is intended to work with the "legacy" representation of a
305305
datastore "Key" used within Google App Engine (a so-called
306306
"Reference"). The returned string can be used as the ``urlsafe``
307-
argument to ``ndb.Key(urlsafe=...)``.
307+
argument to ``ndb.Key(urlsafe=...)``. The base64 encoded values
308+
will have padding removed.
308309
309310
:rtype: bytes
310311
:returns: A bytestring containing the key encoded as URL-safe base64.
@@ -315,7 +316,7 @@ def to_legacy_urlsafe(self):
315316
name_space=self.namespace,
316317
)
317318
raw_bytes = reference.SerializeToString()
318-
return base64.urlsafe_b64encode(raw_bytes)
319+
return base64.urlsafe_b64encode(raw_bytes).strip(b'=')
319320

320321
@classmethod
321322
def from_legacy_urlsafe(cls, urlsafe):
@@ -334,6 +335,8 @@ def from_legacy_urlsafe(cls, urlsafe):
334335
:returns: The key corresponding to ``urlsafe``.
335336
"""
336337
urlsafe = _to_bytes(urlsafe, encoding='ascii')
338+
padding = b'=' * (-len(urlsafe) % 4)
339+
urlsafe += padding
337340
raw_bytes = base64.urlsafe_b64decode(urlsafe)
338341

339342
reference = _app_engine_key_pb2.Reference()

datastore/tests/unit/test_key.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ class TestKey(unittest.TestCase):
2626
# 'Parent', 59, 'Child', 'Feather',
2727
# namespace='space', app='s~sample-app')
2828
# urlsafe = key.urlsafe()
29-
_URLSAFE_EXAMPLE = (
29+
_URLSAFE_EXAMPLE1 = (
3030
b'agxzfnNhbXBsZS1hcHByHgsSBlBhcmVudBg7DAsSBUNoaWxkIgdGZ'
3131
b'WF0aGVyDKIBBXNwYWNl')
32-
_URLSAFE_APP = 's~sample-app'
33-
_URLSAFE_NAMESPACE = 'space'
34-
_URLSAFE_FLAT_PATH = ('Parent', 59, 'Child', 'Feather')
32+
_URLSAFE_APP1 = 's~sample-app'
33+
_URLSAFE_NAMESPACE1 = 'space'
34+
_URLSAFE_FLAT_PATH1 = ('Parent', 59, 'Child', 'Feather')
35+
_URLSAFE_EXAMPLE2 = b'agZzfmZpcmVyDwsSBEtpbmQiBVRoaW5nDA'
36+
_URLSAFE_APP2 = 's~fire'
37+
_URLSAFE_FLAT_PATH2 = ('Kind', 'Thing')
3538

3639
@staticmethod
3740
def _get_target_class():
@@ -388,25 +391,45 @@ def test_to_protobuf_w_no_kind(self):
388391

389392
def test_to_legacy_urlsafe(self):
390393
key = self._make_one(
391-
*self._URLSAFE_FLAT_PATH,
392-
project=self._URLSAFE_APP,
393-
namespace=self._URLSAFE_NAMESPACE)
394+
*self._URLSAFE_FLAT_PATH1,
395+
project=self._URLSAFE_APP1,
396+
namespace=self._URLSAFE_NAMESPACE1)
394397
# NOTE: ``key.project`` is somewhat "invalid" but that is OK.
395398
urlsafe = key.to_legacy_urlsafe()
396-
self.assertEqual(urlsafe, self._URLSAFE_EXAMPLE)
399+
self.assertEqual(urlsafe, self._URLSAFE_EXAMPLE1)
400+
401+
def test_to_legacy_urlsafe_strip_padding(self):
402+
key = self._make_one(
403+
*self._URLSAFE_FLAT_PATH2,
404+
project=self._URLSAFE_APP2)
405+
# NOTE: ``key.project`` is somewhat "invalid" but that is OK.
406+
urlsafe = key.to_legacy_urlsafe()
407+
self.assertEqual(urlsafe, self._URLSAFE_EXAMPLE2)
408+
# Make sure it started with base64 padding.
409+
self.assertNotEqual(len(self._URLSAFE_EXAMPLE2) % 4, 0)
397410

398411
def test_from_legacy_urlsafe(self):
399412
klass = self._get_target_class()
400-
key = klass.from_legacy_urlsafe(self._URLSAFE_EXAMPLE)
413+
key = klass.from_legacy_urlsafe(self._URLSAFE_EXAMPLE1)
401414

402-
self.assertEqual('s~' + key.project, self._URLSAFE_APP)
403-
self.assertEqual(key.namespace, self._URLSAFE_NAMESPACE)
404-
self.assertEqual(key.flat_path, self._URLSAFE_FLAT_PATH)
415+
self.assertEqual('s~' + key.project, self._URLSAFE_APP1)
416+
self.assertEqual(key.namespace, self._URLSAFE_NAMESPACE1)
417+
self.assertEqual(key.flat_path, self._URLSAFE_FLAT_PATH1)
405418
# Also make sure we didn't accidentally set the parent.
406419
self.assertIsNone(key._parent)
407420
self.assertIsNotNone(key.parent)
408421
self.assertIs(key._parent, key.parent)
409422

423+
def test_from_legacy_urlsafe_needs_padding(self):
424+
klass = self._get_target_class()
425+
# Make sure it will have base64 padding added.
426+
self.assertNotEqual(len(self._URLSAFE_EXAMPLE2) % 4, 0)
427+
key = klass.from_legacy_urlsafe(self._URLSAFE_EXAMPLE2)
428+
429+
self.assertEqual('s~' + key.project, self._URLSAFE_APP2)
430+
self.assertIsNone(key.namespace)
431+
self.assertEqual(key.flat_path, self._URLSAFE_FLAT_PATH2)
432+
410433
def test_is_partial_no_name_or_id(self):
411434
key = self._make_one('KIND', project=self._DEFAULT_PROJECT)
412435
self.assertTrue(key.is_partial)

0 commit comments

Comments
 (0)