Как декодировать AppEngine db.Key Protobuf без библиотек

Таким образом, для тех, кто использовал ключи базы данных AppEngine, вы можете быть знакомы с тем, что глобально уникальный uuid каждой сущности представляет собой закодированную в base64 строку из нескольких основных элементов информации, а именно: имя приложения, имя модели и его id_or_name.

Это позволяет AppEngine находить любой объект базы данных в мире с помощью небольшой простой строки. На самом деле, все типы ReferenceProperty в AppEngine просто хранятся как эта одна и та же строка base64, поэтому ее можно очень быстро разрешить в мире нереляционных баз данных. Поскольку это основа хранилища данных, имеет смысл упаковать всю эту информацию в как можно меньшее количество байтов, поэтому, естественно, Google использует буферы протокола для кодирования объекта db.Key в строку для отправки. провод.

Итак, что произойдет, если вам дадут один из этих неприятных ключей, и вам нужно выполнить соединение BigQuery с entity.key().id_or_name()? Или ваш клиент Javascript просто хочет получить тип модели или ее идентификатор на стороне клиента без RPC, а ваш JSON не сериализует эти удобные вещи? Или что, если вам просто чертовски любопытно, как на самом деле закодированы эти байты!

Удивительно, но AppEngine предоставляет полный SDK для локального запуска всего стека на вашем компьютере и даже для загрузки всего этого в UnitTest, чтобы мы могли пройти через то, что происходит на стороне сервера каждый раз, когда создается экземпляр ключа.

Итак, приступим к делу…

Сначала мы начнем с любой строки db.Key.

Теперь волшебство начинает происходить глубоко в entity_pb.py, где он начинает обрабатывать строковый буфер:

Синтаксический анализатор в основном перескакивает с проверки байта, чтобы увидеть, какой у него тип и какова его длина, захватывает данные сразу после этого, а затем продолжает пыхтеть вперед. Наш первый байт 'j' на самом деле вообще не является символом (помните, что 1 байт может представлять 256 частей информации, но только подмножество этих байтов на самом деле является печатаемыми символами в ASCII. Вся строка на самом деле представляет собой просто смесь сигналов). байты, строки и числа переменной длины Во многих случаях проще думать об их шестнадцатеричном коде или порядковом значении… j = '106'.

И, как вы можете видеть, это синтаксический анализатор, пытающийся вычислить часть имени приложения в ключе. Но давайте перейдем к первому получению типа модели, который представляет собой строку переменной длины.

Все это происходит в магическом байте 0x12 (базовое значение 10 равно 18), где мы теперь знаем, что собираемся проанализировать строку. Следующий байт после этого говорит нам, сколько байтов длина составляет наша строка: ‘\n’, что на самом деле просто python, пытающийся отобразить байты в ascii, когда это возможно. ord('\n') равен 0x0a или 10.

Это 10 за 10 символов в «HelloWorld»!

Теперь все было довольно прямолинейно, верно?

Теперь получить идентификатор объекта немного сложнее. Это числа Int64 после их загрузки в память или bigquery, но по сети они кодируются немного по-другому. Вместо стандартных 8 байт число кодируется более гибко. В основном у нас есть 7 бит данных, и каждый байт имеет завершающий 8-й бит. Итак, пока 8-й бит равен «1», мы продолжаем захватывать следующий байт из буфера, объединяя все 7 бит, которые мы прошли по пути, сдвигая их по битам в Int64.

Давайте посмотрим на пример

Затем мы сталкиваемся с другим магическим байтом 0x18, который равен 24 по основанию 10, и теперь мы знаем, что у нас есть Int64 переменной длины, идущий по конвейеру.

Таким образом, в стране беззнаковых байтов, поскольку любой байт с «1» в качестве 8-го бита равен ≥ 128, мы в основном продолжаем читать байты, пока не найдем число меньшее, чем это, что часто является печатным символом ascii, в данном случае этот байт будет '\ x15', который является непечатаемым символом NAK (отрицательное подтверждение)

А вот реальный цикл побитового сдвига массива байтов:

И вуаля! У нас есть тот же entity.key().id(), который мы распечатали в начале в консоли.

Итак, попробуйте сами — возьмите любую строку с ключом, откройте интерпретатор Python без загрузки каких-либо библиотек и запустите это:

# in py2 
import base64
key = ‘agd0ZXN0YmVkchcLEgpIZWxsb1dvcmxkGIeXrevFivcVDA’
urlsafe = str(key)
mod = len(urlsafe) % 4
urlsafe += (‘=’*(4-mod)) if mod else ‘’
b = base64.b64decode(urlsafe.replace(‘-’, ‘+’).replace(‘_’, ‘/’))
id_bytes = b.split(‘\x18’)[-1][0:-1]
ba = bytearray(id_bytes)
length = ord(b.split(‘\x12’)[1][0])
db_type = b.split(‘\x12’)[1][1:length+1]
db_id = sum([(ba[i] & 127) << i*7 for i in xrange(len(ba))])
print ‘%s:%s’ % (db_type, db_id)

# Here's a function declaration that works for id_or_name in py2+3
def decode_key(key):
  # turn b64 string into some bytes
  urlsafe = str(key)
  mod = len(urlsafe) % 4
  urlsafe += ('='*(4-mod)) if mod else ''
  b = base64.b64decode(urlsafe.replace('-', '+').replace('_', '/'))

  # skip ahead to where the class name/id bytes are
  cls_and_id = b.split(b'\x0b\x12')[1]
  cls_len = cls_and_id[0] # ord() this in py2
  db_type = cls_and_id[1:cls_len + 1].decode()

  # figure out if this is id or name
  id_type = cls_and_id[cls_len + 1] # ord() this in py2
  assert id_type in [34, 24], "key fail id_type=%s" % id_type
  id_or_name = cls_and_id[cls_len + 2:]

  if id_type == 34: # name()
    name_len = id_or_name[0] # ord() this in py2
    db_id = id_or_name[1:name_len + 1].decode()
  else: # id()
    ba = bytearray(id_or_name[0:-1])
    db_id = sum([(ba[i] & 127) << i*7 for i in range(len(ba))])

  return db_type, db_id

Пролог:

Это просто теоретическое упражнение, чтобы понять, что происходит под капотом, и я бы оставил эту технику только для отладки или временной меры. Хотя я нахожу это весьма полезным на протяжении многих лет, когда у меня есть отношение 1: 1 объекта к другому, когда я хочу, чтобы массовый поиск key_name был быстрым, делая key_name производного объекта самим ключом, я обычно попробуйте использовать то, что я называю short_key ключа сущности. например

Кроме того, когда в тех случаях, когда у меня есть структура наследования в моем дизайне модели, вы обнаружите, что необходимо возвращать клиентам ключи вместо id_or_name() в JSON — здесь я тоже начал использовать короткие_ключи. Пока все сущности работают в одном и том же приложении AppEngine, в большинстве случаев получается меньше версии protobuf, а также ее намного проще читать и отлаживать.

Например, создав key_name объекта, который представляет принадлежность некоторого пользовательского объекта объекту канала:

Теперь я могу найти объект членства без медленного составного запроса… и я действительно могу понять свой ответ JSON.