import os import struct import sys from typing import Any, Dict, List, Optional, Tuple from .types import Matrix, Color, Point, Rectangle from .types import AP2Action, AP2Tag, AP2Property from .util import TrackedCoverage, VerboseOutput, _hex class NamedTagReference: def __init__(self, swf_name: str, tag_name: str) -> None: self.swf = swf_name self.tag = tag_name def as_dict(self) -> Dict[str, Any]: return { 'swf': self.swf, 'tag': self.tag, } def __repr__(self) -> str: return f"{self.swf}.{self.tag}" class ByteCode: # A list of bytecodes to execute. def __init__(self) -> None: # TODO: I need to actually come up with some internal representation of this. As far # as I can tell, games execute bytecode by switch statement dirctly loading the opcodes # from memory. There is no dynamic recompilation happening and they don't parse anything. pass class TagPointer: # A pointer to a tag in this SWF by Tag ID and containing an optional initialization bytecode # to run for this tag when it is placed/executed. def __init__(self, id: Optional[int], init_bytecode: Optional[ByteCode] = None) -> None: self.id = id self.init_bytecode = init_bytecode class Frame: def __init__(self, start_tag_offset: int, num_tags: int, imported_tags: List[TagPointer] = []) -> None: # The start tag offset into the tag list where we should begin placing/executing tags for this frame. self.start_tag_offset = start_tag_offset # The number of tags to pace/execute during this frame. self.num_tags = num_tags # A list of any imported tags that are to be placed this frame. self.imported_tags = imported_tags class Tag: # Any tag that can appear in the SWF. All tags will subclass from this for their behavior. def __init__(self, id: Optional[int]) -> None: self.id = id def children(self) -> List["Tag"]: return [] class AP2ShapeTag(Tag): def __init__(self, id: int, reference: str) -> None: super().__init__(id) # The reference is the name of a shape (geo structure) that defines this primitive or sprite. self.reference = reference class AP2DefineFontTag(Tag): def __init__(self, id: int, fontname: str, xml_prefix: str, heights: List[int]) -> None: super().__init__(id) # The font name is just the pretty name of the font. self.fontname = fontname # The XML prefix is the reference into any font XML to look up individual # glyphs for a font in a texture map. self.xml_prefix = xml_prefix # The list of heights are concatenated with the above XML prefix and the # unicode glyph you want to display, to find the corresponding location # in the texture map. self.heights = heights class AP2DoActionTag(Tag): def __init__(self, bytecode: ByteCode) -> None: # Do Action Tags are not identified by any tag ID. super().__init__(None) # The bytecode is the actual execution that we expect to perform once # this tag is placed/executed. self.bytecode = bytecode class AP2PlaceObjectTag(Tag): def __init__( self, object_id: int, depth: int, src_tag_id: Optional[int], name: Optional[str], blend: Optional[int], update: bool, transform: Optional[Matrix], rotation_offset: Optional[Point], mult_color: Optional[Color], add_color: Optional[Color], triggers: Dict[int, List[ByteCode]], ) -> None: # Place Object Tags are not identified by any tag ID. super().__init__(None) # The object ID that we should associate with this object, for removal # and presumably update and other uses. Not the same as Tag ID. self.object_id = object_id # The depth (level) that we should remove objects from. self.depth = depth # The source tag ID (should point at an AP2ShapeTag by ID) if present. self.source_tag_id = src_tag_id # The name of this object, if present. self.name = name # The blend mode of this object, if present. self.blend = blend # Whether this is an object update (True) or a new object (False). self.update = update # Whether there is a transform matrix to apply before placing/updating this object or not. self.transform = transform self.rotation_offset = rotation_offset # If there is a color to blend with the sprite/shape when drawing. self.mult_color = mult_color # If there is a color to add with the sprite/shape when drawing. self.add_color = add_color # List of triggers for this object, and their respective bytecodes to execute when the trigger # fires. self.triggers = triggers class AP2RemoveObjectTag(Tag): def __init__(self, object_id: int, depth: int) -> None: # Remove Object Tags are not identified by any tag ID. super().__init__(None) # The object ID that we should remove, or 0 if we should only remove by depth. self.object_id = object_id # The depth (level) that we should remove objects from. self.depth = depth class AP2DefineSpriteTag(Tag): def __init__(self, id: int, tags: List[Tag], frames: List[Frame]) -> None: super().__init__(id) # The list of tags that this sprite consists of. Sprites are, much like vanilla # SWFs, basically entire SWF movies embedded in them. self.tags = tags # The list of frames this SWF occupies. self.frames = frames def children(self) -> List["Tag"]: return self.tags class AP2DefineEditTextTag(Tag): def __init__(self, id: int, font_tag_id: int, font_height: int, rect: Rectangle, color: Color, default_text: Optional[str] = None) -> None: super().__init__(id) # The ID of the Ap2DefineFontTag that we want to use for the text. self.font_tag_id = font_tag_id # The height we want to select for the text (must be one of the heights in # the referenced Ap2DefineFontTag tag). self.font_height = font_height # The bounding rectangle for this exit text control. self.rect = rect # The text color we want to use when displaying the text. self.color = color # The default text that should be present in the control when it is initially placed/executed. self.default_text = default_text class SWF(TrackedCoverage, VerboseOutput): def __init__( self, name: str, data: bytes, descramble_info: bytes = b"", ) -> None: # First, init the coverage engine. super().__init__() # Name of this SWF, according to the container it was extracted from. self.name: str = name # Name of this SWF, as referenced by other SWFs that require imports from it. self.exported_name: str = "" # Full, unparsed data for this SWF, as well as the descrambling headers. self.data: bytes = data self.descramble_info: bytes = descramble_info # Data version of this SWF. self.data_version: int = 0 # Container version of this SWF. self.container_version: int = 0 # The requested frames per second this SWF plays at. self.fps: float = 0.0 # Background color of this SWF movie. self.color: Optional[Color] = None # Location of this SWF in screen space. self.location: Rectangle = Rectangle.Empty() # Exported tags, indexed by their name and pointing to the Tag ID that name identifies. self.exported_tags: Dict[str, int] = {} # Imported tags, indexed by their Tag ID, and pointing at the SWF asset and exported tag name. self.imported_tags: Dict[int, NamedTagReference] = {} # Actual tags for this SWF, ordered by their appearance in the file. self.tags: List[Tag] = [] # Frames of this SWF, with the tag offset in the above list and number of tags to # "execute" that frame. self.frames: List[Frame] = [] # SWF string table. This is used for faster lookup of strings as well as # tracking which strings in the table have been parsed correctly. self.__strings: Dict[int, Tuple[str, bool]] = {} # Whether this is parsed or not. self.parsed = False def print_coverage(self) -> None: # First print uncovered bytes super().print_coverage() # Now, print uncovered strings for offset, (string, covered) in self.__strings.items(): if covered: continue print(f"Uncovered string: {hex(offset)} - {string}", file=sys.stderr) def as_dict(self) -> Dict[str, Any]: return { 'name': self.name, 'data': "".join(_hex(x) for x in self.data), 'descramble_info': "".join(_hex(x) for x in self.descramble_info), } def __parse_bytecode(self, datachunk: bytes, string_offsets: List[int] = [], prefix: str = "") -> ByteCode: # First, we need to check if this is a SWF-style bytecode or an AP2 bytecode. ap2_sentinel = struct.unpack("B", datachunk[offset_ptr:(offset_ptr + 1)])[0] action_name = AP2Action.action_to_name(opcode) # Because the starting offset is non-zero, we calculate this here as a convenience for displaying. It means # that line numbers for opcodes start at 0 but we have to fix up offsets for jumps by the start_offset. lineno = offset_ptr - start_offset if opcode in AP2Action.actions_without_params(): self.vprint(f"{prefix} {lineno}: {action_name}") offset_ptr += 1 elif opcode == AP2Action.DEFINE_FUNCTION2: function_flags, funcname_offset, bytecode_offset, _, bytecode_count = struct.unpack( ">HHHBH", datachunk[(offset_ptr + 1):(offset_ptr + 10)], ) if funcname_offset == 0: funcname = "" else: funcname = self.__get_string(funcname_offset) offset_ptr += 10 + (3 * bytecode_offset) self.vprint(f"{prefix} {lineno}: {action_name} Flags: {hex(function_flags)}, Name: {funcname}, ByteCode Offset: {hex(bytecode_offset)}, ByteCode Length: {hex(bytecode_count)}") # TODO: Need to do something with this parsed function bytecode. function = self.__parse_bytecode(datachunk[offset_ptr:(offset_ptr + bytecode_count)], string_offsets=string_offsets, prefix=prefix + " ") # NOQA self.vprint(f"{prefix} END_{action_name}") offset_ptr += bytecode_count elif opcode == AP2Action.PUSH: obj_count = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 self.vprint(f"{prefix} {lineno}: {action_name}") while obj_count > 0: obj_to_create = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] offset_ptr += 1 if obj_to_create == 0x0: # Integer "0" object. self.vprint(f"{prefix} INTEGER: 0") elif obj_to_create == 0x1: # Float object, represented internally as a double. fval = struct.unpack(">f", datachunk[offset_ptr:(offset_ptr + 4)])[0] offset_ptr += 4 self.vprint(f"{prefix} FLOAT: {fval}") elif obj_to_create == 0x2: # Null pointer object. self.vprint(f"{prefix} NULL") elif obj_to_create == 0x3: # Undefined constant. self.vprint(f"{prefix} UNDEFINED") elif obj_to_create == 0x4: # Register value. regno = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] offset_ptr += 1 self.vprint(f"{prefix} REGISTER NO: {regno}") elif obj_to_create == 0x5: # Boolean "TRUE" object. self.vprint(f"{prefix} BOOLEAN: True") elif obj_to_create == 0x6: # Boolean "FALSE" object. self.vprint(f"{prefix} BOOLEAN: False") elif obj_to_create == 0x7: # Integer object. ival = struct.unpack(">i", datachunk[offset_ptr:(offset_ptr + 4)])[0] offset_ptr += 4 self.vprint(f"{prefix} INTEGER: {ival}") elif obj_to_create == 0x8: # String constant object. const_offset = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] const = self.__get_string(string_offsets[const_offset]) offset_ptr += 1 self.vprint(f"{prefix} STRING CONST: {const}") elif obj_to_create == 0x9: # String constant, but with 16 bits for the offset. Probably not used except # on the largest files. const_offset = struct.unpack(">H", datachunk[offset_ptr:(offset_ptr + 2)])[0] const = self.__get_string(string_offsets[const_offset]) offset_ptr += 2 self.vprint(f"{prefix} STRING_CONTS: {const}") elif obj_to_create == 0xa: # NaN constant. self.vprint(f"{prefix} NAN") elif obj_to_create == 0xb: # Infinity constant. self.vprint(f"{prefix} INFINITY") elif obj_to_create == 0xc: # Pointer to "this" object, whatever currently is executing the bytecode. self.vprint(f"{prefix} POINTER TO THIS") elif obj_to_create == 0xd: # Pointer to "root" object, which is the movieclip this bytecode exists in. self.vprint(f"{prefix} POINTER TO ROOT") elif obj_to_create == 0xe: # Pointer to "parent" object, whatever currently is executing the bytecode. # This seems to be the parent of the movie clip, or the current movieclip # if that isn't set. self.vprint(f"{prefix} POINTER TO PARENT") elif obj_to_create == 0xf: # Current movie clip. self.vprint(f"{prefix} POINTER TO CURRENT MOVIECLIP") elif obj_to_create == 0x10: # Property constant with no alias. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x100 offset_ptr += 1 self.vprint(f"{prefix} PROPERTY CONST NAME: {AP2Property.property_to_name(propertyval)}") elif obj_to_create == 0x11: # Property constant referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x100 referenceval = self.__get_string(string_offsets[reference]) offset_ptr += 2 self.vprint(f"{prefix} PROPERTY CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") elif obj_to_create == 0x12: # Same as above, but with allowance for a 16-bit constant offset. propertyval, reference = struct.unpack(">BH", datachunk[offset_ptr:(offset_ptr + 3)]) propertyval += 0x100 referenceval = self.__get_string(string_offsets[reference]) offset_ptr += 3 self.vprint(f"{prefix} PROPERTY CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") elif obj_to_create == 0x13: # Class property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x300 offset_ptr += 1 self.vprint(f"{prefix} CLASS CONST NAME: {AP2Property.property_to_name(propertyval)}") elif obj_to_create == 0x14: # Class property constant with alias. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x300 referenceval = self.__get_string(string_offsets[reference]) offset_ptr += 2 self.vprint(f"{prefix} CLASS CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") # One would expect 0x15 to be identical to 0x12 but for class properties instead. However, it appears # that this has been omitted from game binaries. elif obj_to_create == 0x16: # Func property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x400 offset_ptr += 1 self.vprint(f"{prefix} FUNC CONST NAME: {AP2Property.property_to_name(propertyval)}") elif obj_to_create == 0x17: # Func property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x400 referenceval = self.__get_string(string_offsets[reference]) offset_ptr += 2 self.vprint(f"{prefix} FUNC CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15 applies here with 0x18. elif obj_to_create == 0x19: # Other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x200 offset_ptr += 1 self.vprint(f"{prefix} OTHER CONST NAME: {AP2Property.property_to_name(propertyval)}") elif obj_to_create == 0x1a: # Other property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x200 referenceval = self.__get_string(string_offsets[reference]) offset_ptr += 2 self.vprint(f"{prefix} OTHER CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15 and 0x18 applies here with 0x1b. elif obj_to_create == 0x1c: # Event property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x500 offset_ptr += 1 self.vprint(f"{prefix} EVENT CONST NAME: {AP2Property.property_to_name(propertyval)}") elif obj_to_create == 0x1d: # Event property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x500 referenceval = self.__get_string(string_offsets[reference]) offset_ptr += 2 self.vprint(f"{prefix} EVENT CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15, 0x18 and 0x1b applies here with 0x1e. elif obj_to_create == 0x1f: # Key constants. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x600 offset_ptr += 1 self.vprint(f"{prefix} KEY CONST NAME: {AP2Property.property_to_name(propertyval)}") elif obj_to_create == 0x20: # Key property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x600 referenceval = self.__get_string(string_offsets[reference]) offset_ptr += 2 self.vprint(f"{prefix} KEY CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15, 0x18, 0x1b and 0x1e applies here with 0x21. elif obj_to_create == 0x22: # Pointer to global object. self.vprint(f"{prefix} POINTER TO GLOBAL OBJECT") elif obj_to_create == 0x23: # Negative infinity. self.vprint(f"{prefix} -INFINITY") elif obj_to_create == 0x24: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x700 offset_ptr += 1 self.vprint(f"{prefix} ETC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") # Possibly in newer binaries, 0x25 and 0x26 are implemented as 8-bit and 16-bit alias pointer # versions of 0x24. elif obj_to_create == 0x27: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x800 offset_ptr += 1 self.vprint(f"{prefix} ORGFUNC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") # Possibly in newer binaries, 0x28 and 0x29 are implemented as 8-bit and 16-bit alias pointer # versions of 0x27. elif obj_to_create == 0x2a: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x900 offset_ptr += 1 self.vprint(f"{prefix} ETCFUNC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") # Possibly in newer binaries, 0x2b and 0x2c are implemented as 8-bit and 16-bit alias pointer # versions of 0x2a. elif obj_to_create == 0x2d: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xa00 offset_ptr += 1 self.vprint(f"{prefix} EVENT2 CONST NAME: {AP2Property.property_to_name(propertyval)}") # Possibly in newer binaries, 0x2e and 0x2f are implemented as 8-bit and 16-bit alias pointer # versions of 0x2d. elif obj_to_create == 0x30: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xb00 offset_ptr += 1 self.vprint(f"{prefix} EVENT METHOD CONST NAME: {AP2Property.property_to_name(propertyval)}") # Possibly in newer binaries, 0x31 and 0x32 are implemented as 8-bit and 16-bit alias pointer # versions of 0x30. elif obj_to_create == 0x33: # Signed 64 bit integer init. Uses special "S64" type. int64 = struct.unpack(">q", datachunk[offset_ptr:(offset_ptr + 8)]) offset_ptr += 8 self.vprint(f"{prefix} INTEGER: {int64}") elif obj_to_create == 0x34: # Some other property names. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xc00 offset_ptr += 1 self.vprint(f"{prefix} GENERIC CONST NAME: {AP2Property.property_to_name(propertyval)}") # Possibly in newer binaries, 0x35 and 0x36 are implemented as 8-bit and 16-bit alias pointer # versions of 0x34. elif obj_to_create == 0x37: # Integer object but one byte. ival = struct.unpack(">b", datachunk[offset_ptr:(offset_ptr + 1)])[0] offset_ptr += 1 self.vprint(f"{prefix} INTEGER: {ival}") elif obj_to_create == 0x38: # Some other property names. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xd00 offset_ptr += 1 self.vprint(f"{prefix} GENERIC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") # Possibly in newer binaries, 0x39 and 0x3a are implemented as 8-bit and 16-bit alias pointer # versions of 0x38. else: raise Exception(f"Unsupported object {hex(obj_to_create)} to push!") obj_count -= 1 self.vprint(f"{prefix} END_{action_name}") elif opcode == AP2Action.INIT_REGISTER: obj_count = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 self.vprint(f"{prefix} {lineno}: {action_name}") while obj_count > 0: register_no = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] offset_ptr += 1 obj_count -= 1 self.vprint(f"{prefix} REGISTER NO: {register_no}") self.vprint(f"{prefix} END_{action_name}") elif opcode == AP2Action.STORE_REGISTER: obj_count = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 self.vprint(f"{prefix} {lineno}: {action_name}") while obj_count > 0: register_no = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] offset_ptr += 1 obj_count -= 1 self.vprint(f"{prefix} REGISTER NO: {register_no}") self.vprint(f"{prefix} END_{action_name}") elif opcode == AP2Action.STORE_REGISTER2: register_no = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 self.vprint(f"{prefix} {lineno}: {action_name}") self.vprint(f"{prefix} REGISTER NO: {register_no}") self.vprint(f"{prefix} END_{action_name}") elif opcode == AP2Action.IF: jump_if_true_offset = struct.unpack(">H", datachunk[(offset_ptr + 1):(offset_ptr + 3)])[0] offset_ptr += 3 # TODO: This can jump outside of a function definition, most commonly seen when jumping to an # "END" pointer at the end of a chunk. We need to handle this. We probably need function lines # to be absolute instead of relative. jump_if_true_offset += offset_ptr - start_offset self.vprint(f"{prefix} {lineno}: Offset If True: {jump_if_true_offset}") elif opcode == AP2Action.IF2: if2_type, jump_if_true_offset = struct.unpack(">BH", datachunk[(offset_ptr + 1):(offset_ptr + 4)]) offset_ptr += 4 # TODO: This can jump outside of a function definition, most commonly seen when jumping to an # "END" pointer at the end of a chunk. We need to handle this. We probably need function lines # to be absolute instead of relative. jump_if_true_offset += offset_ptr - start_offset if2_typestr = { 0: "==", 1: "!=", 2: "<", 3: ">", 4: "<=", 5: ">=", 6: "!", 7: "BITAND", 8: "BITNOTAND", 9: "STRICT ==", 10: "STRICT !=", 11: "IS UNDEFINED", 12: "IS NOT UNDEFINED", }[if2_type] self.vprint(f"{prefix} {lineno}: {action_name} {if2_typestr}, Offset If True: {jump_if_true_offset}") elif opcode == AP2Action.JUMP: jump_offset = struct.unpack(">H", datachunk[(offset_ptr + 1):(offset_ptr + 3)])[0] offset_ptr += 3 # TODO: This can jump outside of a function definition, most commonly seen when jumping to an # "END" pointer at the end of a chunk. We need to handle this. We probably need function lines # to be absolute instead of relative. jump_offset += offset_ptr - start_offset self.vprint(f"{prefix} {lineno}: {action_name} Offset: {jump_offset}") elif opcode == AP2Action.WITH: skip_offset = struct.unpack(">H", datachunk[(offset_ptr + 1):(offset_ptr + 3)])[0] offset_ptr += 3 # TODO: I have absolutely no idea what the data which exists in the bytecode buffer at this point # represents... unknown_data = datachunk[offset_ptr:(offset_ptr + skip_offset)] # NOQA offset_ptr += skip_offset self.vprint(f"{prefix} {lineno}: {action_name} Unknown Data Length: {skip_offset}") elif opcode == AP2Action.ADD_NUM_VARIABLE: amount_to_add = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 self.vprint(f"{prefix} {lineno}: {action_name} Add Value: {amount_to_add}") elif opcode == AP2Action.GET_URL2: action = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 self.vprint(f"{prefix} {lineno}: {action_name} URL Action: {action >> 6}") elif opcode == AP2Action.START_DRAG: constraint = struct.unpack(">b", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 self.vprint(f"{prefix} {lineno}: {action_name} Constrain Mouse: {'yes' if constraint > 0 else ('no' if constraint == 0 else 'check stack')}") elif opcode == AP2Action.ADD_NUM_REGISTER: register_no, amount_to_add = struct.unpack(">BB", datachunk[(offset_ptr + 1):(offset_ptr + 3)]) offset_ptr += 3 self.vprint(f"{prefix} {lineno}: {action_name} Register No: {register_no}, Add Value: {amount_to_add}") elif opcode == AP2Action.GOTO_FRAME2: flags = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 if flags & 0x1: post = "STOP" else: post = "PLAY" if flags & 0x2: # Additional frames to add on top of stack value. additional_frames = struct.unpack(">H", datachunk[offset_ptr:(offset_ptr + 2)])[0] offset_ptr += 2 else: additional_frames = 0 self.vprint(f"{prefix} {lineno}: {action_name} AND {post} Additional Frames: {additional_frames}") else: raise Exception(f"Can't advance, no handler for opcode {opcode} ({hex(opcode)})!") # TODO: I need to actually parse this, its a mess... return ByteCode() def __parse_tag(self, ap2_version: int, afp_version: int, ap2data: bytes, tagid: int, size: int, dataoffset: int, prefix: str = "") -> Tag: if tagid == AP2Tag.AP2_SHAPE: if size != 4: raise Exception(f"Invalid shape size {size}") unknown, shape_id = struct.unpack(" 0: catchup = 4 - misalignment self.add_coverage(dataoffset + running_pointer, catchup) running_pointer += catchup # Handle transformation matrix. transform = Matrix.identity() if flags & 0x100: # Has scale component. unhandled_flags &= ~0x100 a_int, d_int = struct.unpack("> 24) & 0xFF) * 0.003921569 multcolor.g = float((rgba >> 16) & 0xFF) * 0.003921569 multcolor.b = float((rgba >> 8) & 0xFF) * 0.003921569 multcolor.a = float(rgba & 0xFF) * 0.003921569 self.vprint(f"{prefix} Mult Color: {multcolor}") if flags & 0x4000: # Additive color present, smaller integers. unhandled_flags &= ~0x4000 rgba = struct.unpack("> 24) & 0xFF) * 0.003921569 addcolor.g = float((rgba >> 16) & 0xFF) * 0.003921569 addcolor.b = float((rgba >> 8) & 0xFF) * 0.003921569 addcolor.a = float(rgba & 0xFF) * 0.003921569 self.vprint(f"{prefix} Add Color: {addcolor}") bytecodes: Dict[int, List[ByteCode]] = {} if flags & 0x80: # Object event triggers. unhandled_flags &= ~0x80 event_flags, event_size = struct.unpack("> 8) & 0xFF) / 255.0, b=((rgba >> 16) & 0xFF) / 255.0, a=((rgba >> 24) & 0xFF) / 255.0, ) self.vprint(f"{prefix} Text Color: {color}") self.vprint(f"{prefix} Unk1: {unk1}, Unk2: {unk2}, Unk3: {unk3}, Unk4: {unk4}") # flags & 0x20 means something with offset 16-18. # flags & 0x200 is unk str below is a HTML tag. if flags & 0x80: # Has some sort of string pointer. default_text = self.__get_string(default_text_offset) or None self.vprint(f"{prefix} Default Text: {default_text}") else: default_text = None return AP2DefineEditTextTag(edit_text_id, defined_font_tag_id, font_height, rect, color, default_text=default_text) else: raise Exception(f"Unimplemented tag {hex(tagid)}!") def __parse_tags(self, ap2_version: int, afp_version: int, ap2data: bytes, tags_base_offset: int, prefix: str = "") -> Tuple[List[Tag], List[Frame]]: unknown_tags_flags, unknown_tags_count, frame_count, tags_count, unknown_tags_offset, frame_offset, tags_offset = struct.unpack( "> 22) & 0x3FF size = tag & 0x3FFFFF if size > 0x200000: raise Exception(f"Invalid tag size {size} ({hex(size)})") self.vprint(f"{prefix} Tag: {hex(tagid)} ({AP2Tag.tag_to_name(tagid)}), Size: {hex(size)}, Offset: {hex(tags_offset + 4)}") tags.append(self.__parse_tag(ap2_version, afp_version, ap2data, tagid, size, tags_offset + 4, prefix=prefix)) tags_offset += ((size + 3) & 0xFFFFFFFC) + 4 # Skip past tag header and data, rounding to the nearest 4 bytes. # Now, parse frames. frames: List[Frame] = [] self.vprint(f"{prefix}Number of Frames: {frame_count}") for i in range(frame_count): frame_info = struct.unpack("> 20) & 0xFFF frames.append(Frame(start_tag_offset, num_tags_to_play)) self.vprint(f"{prefix} Frame Start Tag: {start_tag_offset}, Count: {num_tags_to_play}") frame_offset += 4 # Now, parse unknown tags? I have no idea what these are, but they're referencing strings that # are otherwise unused. self.vprint(f"{prefix}Number of Unknown Tags: {unknown_tags_count}, Flags: {hex(unknown_tags_flags)}") for i in range(unknown_tags_count): unk1, stringoffset = struct.unpack(" bytes: swap_len = { 1: 2, 2: 4, 3: 8, } data = bytearray(scrambled_data) data_offset = 0 for i in range(0, len(descramble_info), 2): swapword = struct.unpack("> 13) & 0x7 loops = ((swapword >> 7) & 0x3F) data_offset += offset if swap_type == 0: # Just jump forward based on loops data_offset += 256 * loops continue if swap_type not in swap_len: raise Exception(f"Unknown swap type {swap_type}!") # Reverse the bytes for _ in range(loops + 1): data[data_offset:(data_offset + swap_len[swap_type])] = data[data_offset:(data_offset + swap_len[swap_type])][::-1] data_offset += swap_len[swap_type] return bytes(data) def __descramble_stringtable(self, scrambled_data: bytes, stringtable_offset: int, stringtable_size: int) -> bytes: data = bytearray(scrambled_data) curstring: List[int] = [] curloc = stringtable_offset addition = 128 for i in range(stringtable_size): byte = (data[stringtable_offset + i] - addition) & 0xFF data[stringtable_offset + i] = byte addition += 1 if byte == 0: if curstring: # We found a string! self.__strings[curloc - stringtable_offset] = (bytes(curstring).decode('utf8'), False) curloc = stringtable_offset + i + 1 curstring = [] curloc = stringtable_offset + i + 1 else: curstring.append(byte) if curstring: raise Exception("Logic error!") if 0 in self.__strings: raise Exception("Should not include null string!") return bytes(data) def __get_string(self, offset: int) -> str: if offset == 0: return "" self.__strings[offset] = (self.__strings[offset][0], True) return self.__strings[offset][0] def parse(self, verbose: bool = False) -> None: with self.covered(len(self.data), verbose): with self.debugging(verbose): self.__parse(verbose) def __parse(self, verbose: bool) -> None: # First, use the byteswap header to descramble the data. data = self.__descramble(self.data, self.descramble_info) # Start with the basic file header. magic, length, version, nameoffset, flags, left, right, top, bottom = struct.unpack("<4sIHHIHHHH", data[0:24]) self.add_coverage(0, 24) ap2_data_version = magic[0] & 0xFF magic = bytes([magic[3] & 0x7F, magic[2] & 0x7F, magic[1] & 0x7F, 0x0]) if magic != b'AP2\x00': raise Exception(f"Unrecognzied magic {magic}!") if length != len(data): raise Exception(f"Unexpected length in AFP header, {length} != {len(data)}!") if ap2_data_version not in [8, 9, 10]: raise Exception(f"Unsupported AP2 container version {ap2_data_version}!") if version != 0x200: raise Exception(f"Unsupported AP2 version {version}!") # The container version is analogous to the SWF file version. I'm pretty sure it # dictates certain things like what properties are available. These appear strictly # additive so we don't concern ourselves with this. self.container_version = ap2_data_version # The data version is basically used for how to parse tags. There was an older data # version 0x100 that used more SWF-like bit-packed tags and while lots of code exists # to parse this, the AP2 libraries will reject SWF data with this version. self.data_version = version # As far as I can tell, most things only care about the width and height of this # movie, and I think the Shapes are rendered based on the width/height. However, it # can have a non-zero x/y offset and I think this is used when rendering multiple # movie clips? self.location = Rectangle(left=left, right=right, top=top, bottom=bottom) if flags & 0x1: # This appears to be the animation background color. rgba = struct.unpack("> 8) & 0xFF) / 255.0, b=((rgba >> 16) & 0xFF) / 255.0, a=((rgba >> 24) & 0xFF) / 255.0, ) else: self.color = None self.add_coverage(28, 4) if flags & 0x2: # FPS can be either an integer or a float. self.fps = struct.unpack("= len(self.frames): raise Exception(f"Unexpected frame {frame}, we only have {len(self.frames)} frames in this movie!") self.frames[frame].imported_tags.append(TagPointer(tag_id, bytecode)) if verbose: self.print_coverage() self.parsed = True