Source code for pyartifact.deck_encoding.decode

import re
from base64 import b64decode
from typing import List, Tuple

from ..exceptions import InvalidDeckString, DeckDecodeException
from ..types.deck_types import HeroDeckType, CardDeckType, DeckContents

# Version 1: Heroes and Cards
# Version 2: Name, Heroes and Cards
SUPPORTED_VERSIONS = [1, 2]


[docs]def decode_deck_string(deck_code: str) -> DeckContents: """ Takes in deck code, e.g. `ADCJWkTZX05uwGDCRV4XQGy3QGLmqUBg4GQJgGLGgO7AaABR3JlZW4vQmxhY2sgRXhhbXBsZQ__` and decodes it into a dict of name, heroes and cards. :param deck_code: Deck code :return: Deck contents :raises InvalidDeckString: When an invalid deck string is provided, e.g. unknown version, bad checksum etc. :raises DeckDecodeException: When something odd happens while decoding """ return Decoder(deck_code).deck_contents
class Decoder: prefix = 'ADC' def __init__(self, deck_code: str) -> None: """ :param deck_code: Deck code """ self.deck_code = deck_code self._binary = bytearray() @property def deck_contents(self) -> DeckContents: self._decode_string() self._get_version_and_heroes() self._get_lengths_and_calc_checksum() self._parse_deck() return self._deck_contents def _decode_string(self) -> None: if not self.deck_code.startswith(self.prefix): raise InvalidDeckString("The provided deck string doesn't start with a known prefix") # Strip the prefix and turn it into a valid base64 from url-safe string deck_code_b64 = self.deck_code.lstrip(self.prefix).replace('-', '/').replace('_', '=') decoded = b64decode(deck_code_b64) self._binary = bytearray(decoded) # If something funky happened, let the user know that it's terrible if not self._binary: raise InvalidDeckString("No binary data could be decoded from the string") def _parse_deck(self) -> None: # Read the rest of heroes count and get the moved 'cursor' self._heroes_count, self._current_index = self._read_var_encoded(self._version_and_heroes, 3, self._card_bytes_start_index, self._total_card_bytes) # Read the list of heroes heroes: List[HeroDeckType] = [] self._previous_card_base: int = 0 for _ in range(self._heroes_count): hero_turn, hero_card_id = self._read_serialized_card(read_until_index=self._total_card_bytes) heroes.append(HeroDeckType(id=hero_card_id, turn=hero_turn)) # Read the list of cards cards: List[CardDeckType] = [] self._previous_card_base = 0 while self._current_index < self._total_card_bytes: card_count, card_id = self._read_serialized_card(read_until_index=len(self._binary)) cards.append(CardDeckType(id=card_id, count=card_count)) # Read the name if it's present if self._name_length: name = self._binary[-self._name_length:].decode() # WIP simple html sanitizer name = re.sub('<[^<]+?>', '', name) else: name = '' self._deck_contents: DeckContents = DeckContents(name=name, heroes=heroes, cards=cards) def _get_version_and_heroes(self) -> None: """Reads the deck code version and first part of number of heroes from the first byte.""" self._version_and_heroes = self._binary[0] self.version = self._version_and_heroes >> 4 if self.version not in SUPPORTED_VERSIONS: raise InvalidDeckString(f"Deck string has incompatible version '{self.version}', " f"supported versions are: {SUPPORTED_VERSIONS}") def _get_lengths_and_calc_checksum(self) -> None: """ Depending on the deck code version reads where the card information starts, how long is the name of the deck and completes the checksum. """ computed_checksum = 0 if self.version == 1: self._name_length = 0 computed_checksum += self._binary[2] self._card_bytes_start_index = 2 else: self._name_length = self._binary[2] self._card_bytes_start_index = 3 self._total_card_bytes = len(self._binary) - self._name_length computed_checksum = sum(b for b in self._binary[self._card_bytes_start_index:self._total_card_bytes]) if self._binary[1] != (computed_checksum & 0xFF): raise InvalidDeckString("Checksum doesn't check out") def _read_var_encoded(self, base_value: int, base_bits: int, index: int, max_index: int) -> Tuple[int, int]: """ Reads an encoded variable :param base_value: When there was another int the byte at index, what that int was, so we can read the next one :param base_bits: How many bits did the base value have :param index: At which index of the binary data to start :param max_index: At which index must the currently read variable end :return: Tuple of the variable and new position of the index 'cursor' in the binary data """ value = 0 delta_shift = 0 value, unknown_thing = _read_bits_chunk(base_value, base_bits, delta_shift, value) if (base_bits == 0) or unknown_thing: delta_shift += base_bits while True: if index > max_index: raise DeckDecodeException("Couldn't read a variable from the string.") next_byte = self._binary[index] index += 1 value, unknown_thing = _read_bits_chunk(next_byte, 7, delta_shift, value) if not unknown_thing: break delta_shift += 7 return value, index def _read_serialized_card(self, read_until_index: int) -> Tuple[int, int]: """ Reads a serialized card. :param read_until_index: At which index must the card end """ # Check if we aren't already off limits if self._current_index > read_until_index: raise DeckDecodeException("Couldn't read a serialized card") # Header byte of the card contains the count (turn) information if it's lower than 3 # and a first part of card id delta information after that header = self._binary[self._current_index] self._current_index += 1 # Read the card id delta card_id_delta, self._current_index = self._read_var_encoded(header, 5, self._current_index, read_until_index) # Add the previous id to the delta we just decoded card_id = self._previous_card_base + card_id_delta # If the header didn't have the real count information, read it from the next bytes if (header >> 6) == 3: count, self._current_index = self._read_var_encoded(0, 0, self._current_index, read_until_index) else: # If the header had it, shift it and subtract 2 from it count = (header >> 6) + 1 self._previous_card_base = card_id return count, card_id def _read_bits_chunk(chunk: int, numb_bits: int, curr_shift: int, out_bits: int) -> Tuple[int, bool]: """Reads a chunk of bits from a byte.""" continue_bit = 1 << numb_bits new_bits = chunk & (continue_bit - 1) out_bits |= (new_bits << curr_shift) return out_bits, (chunk & continue_bit) != 0