Jak zdekodować protobuf db.Key AppEngine bez bibliotek

Zatem każdy, kto korzysta z kluczy bazy danych AppEngine, może wiedzieć, że unikalny na całym świecie identyfikator UUID każdej jednostki to w zasadzie ciąg kilku podstawowych informacji zakodowany w formacie base64, a mianowicie: nazwa aplikacji, nazwa modelu i jej identyfikator_lub_nazwa

Dzięki temu AppEngine może znaleźć dowolną jednostkę bazy danych na świecie za pomocą ładnego, małego, prostego ciągu znaków. W rzeczywistości wszystkie typy ReferenceProperty w AppEngine są również przechowywane jako ten ten sam ciąg znaków base64, dzięki czemu można je bardzo szybko rozwiązać w świecie nierelacyjnych baz danych. Ponieważ jest to podstawa magazynu danych, sensowne jest spakowanie wszystkich tych informacji w jak najmniejszej liczbie bajtów, więc Google naturalnie używa buforów protokołu do zakodowania obiektu db.Key w ciągu znaków do przesłania drut.

Co więc się stanie, jeśli otrzymasz jeden z tych paskudnych kluczy i będziesz musiał wykonać połączenie BigQuery z obiektem.key().id_or_name()? A może Twój klient JavaScript chce po prostu uzyskać typ modelu lub identyfikator po stronie klienta bez wywołań RPC, a Twój JSON nie ma serializacji tych przydatnych rzeczy? A co, jeśli jesteś po prostu cholernie ciekawy, jak te bajty są naprawdę kodowane!

Co cudowne, AppEngine udostępnia kompletny pakiet SDK, aby uruchomić cały stos lokalnie na Twojej maszynie, a nawet załadować go w teście UnitTest, abyśmy mogli szczegółowo sprawdzić, co dzieje się po stronie serwera za każdym razem, gdy tworzona jest instancja klucza.

Przejdźmy więc do tego…

Najpierw zaczniemy od dowolnego ciągu db.Key

Teraz magia zaczyna dziać się głęboko w Entity_pb.py, gdzie zaczyna przetwarzać bufor ciągów:

Parser zasadniczo przełącza się ze sprawdzania bajtu, aby zobaczyć, jaki jest jego typ i jak długo trwa, pobiera dane natychmiast po tym, a następnie kontynuuje przerzucanie się do przodu. Nasz pierwszy bajt „j” tak naprawdę w ogóle nie jest znakiem (pamiętaj, że 1 bajt może reprezentować 256 informacji, ale tylko podzbiór tych bajtów to w rzeczywistości znaki drukowalne w kodzie ASCII. Cały ciąg znaków jest w rzeczywistości jedynie mieszanką sygnałów sygnalizacyjnych bajty, ciągi znaków i liczby o zmiennej długości W wielu przypadkach łatwiej jest pomyśleć o ich kodzie szesnastkowym lub wartości ord…j = „106”.

Jak widać, jest to parser próbujący znaleźć część klucza zawierającą nazwę aplikacji. Przejdźmy jednak od razu do uzyskania typu modelu, którym jest ciąg znaków o zmiennej długości

Wszystko to dzieje się w magicznym bajcie 0x12 (wartość podstawowa 10 wynosi 18), w którym wiemy, że zaraz będziemy analizować ciąg znaków. Następny bajt mówi nam, ile bajtów długości ma nasz ciąg znaków: „\n”, który w rzeczywistości jest po prostu Pythonem próbującym renderować bajty w formacie ASCII, kiedy tylko może. Ord(‘\n’) to 0x0a lub 10.

To 10 na 10 znaków w „HelloWorld”!

To wszystko było całkiem proste, prawda?

Teraz uzyskanie identyfikatora jednostki jest nieco trudniejsze. Są to liczby Int64 po załadowaniu do pamięci lub BigQuery, ale w sieci są kodowane nieco inaczej. Zamiast standardowych 8 bajtów liczba jest kodowana w bardziej rozszerzalny sposób. Zasadniczo mamy 7 bitów danych, a każdy bajt ma kończący się ósmy bit. Tak długo jak ósmy bit to „1”, pobieramy kolejny bajt z bufora, łącząc wszystkie 7 bitów, które napotkaliśmy po drodze, przesuwając je bitowo do Int64.

Rzućmy okiem na przykład

Następnie napotykamy kolejny magiczny bajt 0x18, który ma wartość 24 w podstawie 10, i teraz wiemy, że mamy do czynienia z int64 o zmiennej długości.

Tak więc w obszarze bajtów bez znaku, ponieważ dowolny bajt z „1” jako ósmy bit ma wartość ≥ 128, zasadniczo czytamy bajty, dopóki nie trafimy na liczbę mniejszą niż ta, która często jest drukowalnym znakiem ascii, w tym przypadku ten bajt będzie to „\x15”, który jest znakiem niedrukowalnym NAK (potwierdzenie negatywne)

A oto rzeczywista pętla polegająca na przesunięciu bitów tablicy bajtów:

I voila! Mamy tę samą jednostkę.key().id(), którą wydrukowaliśmy na początku w konsoli.

Więc śmiało i spróbuj sam — weź dowolny ciąg klawiszy, otwórz interpreter Pythona bez ładowania żadnych bibliotek i uruchom to:

# 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

Prolog:

To po prostu teoretyczne ćwiczenie mające na celu zrozumienie, co dzieje się pod maską, i zarezerwowałbym tę technikę tylko do debugowania lub tymczasowej przerwy. Chociaż stwierdziłem, że jest to całkiem przydatne na przestrzeni lat, gdy mam relację 1:1 między jednostką a inną, gdzie chcę, aby zbiorcze wyszukiwanie nazwy_klucza było szybkie, czyniąc nazwę_klucza jednostki pochodnej samym kluczem, zazwyczaj spróbuj użyć tego, co nazywam krótkim_kluczem klucza jednostki. np.

Ponadto, gdy w moim projekcie modelu mam strukturę dziedziczenia, okaże się konieczne zwrócenie klientom kluczy zamiast id_or_name() w JSON — tutaj również zacząłem używać short_keys. Dopóki wszystkie elementy pracują w tej samej aplikacji AppEngine, w większości przypadków jest to mniej niż wersja protobuf, a także jest znacznie łatwiejsze do odczytania i debugowania

Na przykład, tworząc nazwę klucza encji reprezentującej przynależność jakiegoś obiektu podobnego do użytkownika do obiektu kanału:

Mogę teraz znaleźć jednostkę członkowską bez powolnego zapytania złożonego… i właściwie rozumiem moją odpowiedź JSON