mirror of
https://github.com/DragonMinded/bemaniutils.git
synced 2026-09-10 11:25:12 -05:00
Beginning of an AFP ByteCode decompiler, starting with a massive code reorg and a control flow graph analyzer.
This commit is contained in:
@@ -29,7 +29,7 @@ class PMAN:
|
|||||||
self.flags2 = flags2
|
self.flags2 = flags2
|
||||||
self.flags3 = flags3
|
self.flags3 = flags3
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'flags': [self.flags1, self.flags2, self.flags3],
|
'flags': [self.flags1, self.flags2, self.flags3],
|
||||||
'entries': self.entries,
|
'entries': self.entries,
|
||||||
@@ -64,7 +64,7 @@ class Texture:
|
|||||||
self.compressed = compressed
|
self.compressed = compressed
|
||||||
self.img = imgdata
|
self.img = imgdata
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'width': self.width,
|
'width': self.width,
|
||||||
@@ -85,7 +85,7 @@ class TextureRegion:
|
|||||||
self.right = right
|
self.right = right
|
||||||
self.bottom = bottom
|
self.bottom = bottom
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'texture': self.textureno,
|
'texture': self.textureno,
|
||||||
'left': self.left,
|
'left': self.left,
|
||||||
@@ -117,7 +117,7 @@ class Unknown1:
|
|||||||
if len(data) != 12:
|
if len(data) != 12:
|
||||||
raise Exception("Unexpected length for Unknown1 structure!")
|
raise Exception("Unexpected length for Unknown1 structure!")
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'data': "".join(_hex(x) for x in self.data),
|
'data': "".join(_hex(x) for x in self.data),
|
||||||
@@ -133,7 +133,7 @@ class Unknown2:
|
|||||||
if len(data) != 4:
|
if len(data) != 4:
|
||||||
raise Exception("Unexpected length for Unknown2 structure!")
|
raise Exception("Unexpected length for Unknown2 structure!")
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'data': "".join(_hex(x) for x in self.data),
|
'data': "".join(_hex(x) for x in self.data),
|
||||||
}
|
}
|
||||||
@@ -215,7 +215,7 @@ class TXP2File(TrackedCoverage, VerboseOutput):
|
|||||||
with self.debugging(verbose):
|
with self.debugging(verbose):
|
||||||
self.__parse(verbose)
|
self.__parse(verbose)
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'endian': self.endian,
|
'endian': self.endian,
|
||||||
'features': self.features,
|
'features': self.features,
|
||||||
@@ -223,19 +223,19 @@ class TXP2File(TrackedCoverage, VerboseOutput):
|
|||||||
'obfuscated': self.text_obfuscated,
|
'obfuscated': self.text_obfuscated,
|
||||||
'legacy_lz': self.legacy_lz,
|
'legacy_lz': self.legacy_lz,
|
||||||
'modern_lz': self.modern_lz,
|
'modern_lz': self.modern_lz,
|
||||||
'textures': [tex.as_dict() for tex in self.textures],
|
'textures': [tex.as_dict(*args, **kwargs) for tex in self.textures],
|
||||||
'texturemap': self.texturemap.as_dict(),
|
'texturemap': self.texturemap.as_dict(*args, **kwargs),
|
||||||
'textureregion': [reg.as_dict() for reg in self.texture_to_region],
|
'textureregion': [reg.as_dict(*args, **kwargs) for reg in self.texture_to_region],
|
||||||
'regionmap': self.regionmap.as_dict(),
|
'regionmap': self.regionmap.as_dict(*args, **kwargs),
|
||||||
'swfdata': [data.as_dict() for data in self.swfdata],
|
'swfdata': [data.as_dict(*args, **kwargs) for data in self.swfdata],
|
||||||
'swfmap': self.swfmap.as_dict(),
|
'swfmap': self.swfmap.as_dict(*args, **kwargs),
|
||||||
'fontdata': str(self.fontdata) if self.fontdata is not None else None,
|
'fontdata': str(self.fontdata) if self.fontdata is not None else None,
|
||||||
'shapes': [shape.as_dict() for shape in self.shapes],
|
'shapes': [shape.as_dict(*args, **kwargs) for shape in self.shapes],
|
||||||
'shapemap': self.shapemap.as_dict(),
|
'shapemap': self.shapemap.as_dict(*args, **kwargs),
|
||||||
'unknown1': [unk.as_dict() for unk in self.unknown1],
|
'unknown1': [unk.as_dict(*args, **kwargs) for unk in self.unknown1],
|
||||||
'unknown1map': self.unk_pman1.as_dict(),
|
'unknown1map': self.unk_pman1.as_dict(*args, **kwargs),
|
||||||
'unknown2': [unk.as_dict() for unk in self.unknown2],
|
'unknown2': [unk.as_dict(*args, **kwargs) for unk in self.unknown2],
|
||||||
'unknown2map': self.unk_pman2.as_dict(),
|
'unknown2map': self.unk_pman2.as_dict(*args, **kwargs),
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
282
bemani/format/afp/decompile.py
Normal file
282
bemani/format/afp/decompile.py
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import os
|
||||||
|
from typing import Any, Dict, List, Tuple, cast
|
||||||
|
|
||||||
|
from .types import AP2Action, JumpAction, IfAction
|
||||||
|
from .util import VerboseOutput
|
||||||
|
|
||||||
|
|
||||||
|
class ByteCode:
|
||||||
|
# A list of bytecodes to execute.
|
||||||
|
def __init__(self, actions: List[AP2Action], end_offset: int) -> None:
|
||||||
|
self.actions = actions
|
||||||
|
self.end_offset = end_offset
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
if kwargs.get('decompile_bytecode', False):
|
||||||
|
decompiler = ByteCodeDecompiler(self)
|
||||||
|
code = decompiler.decompile(verbose=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'code': code,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'actions': [a.as_dict(*args, **kwargs) for a in self.actions],
|
||||||
|
'end_offset': self.end_offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
entries: List[str] = []
|
||||||
|
for action in self.actions:
|
||||||
|
entries.extend([f" {s}" for s in str(action).split(os.linesep)])
|
||||||
|
|
||||||
|
return f"ByteCode({os.linesep}{os.linesep.join(entries)}{os.linesep} {self.end_offset}: END{os.linesep})"
|
||||||
|
|
||||||
|
|
||||||
|
class ControlFlow:
|
||||||
|
def __init__(self, beginning: int, end: int, next_flow: List[int]) -> None:
|
||||||
|
self.beginning = beginning
|
||||||
|
self.end = end
|
||||||
|
self.next_flow = next_flow
|
||||||
|
|
||||||
|
def contains(self, offset: int) -> bool:
|
||||||
|
return (self.beginning <= offset) and (offset < self.end)
|
||||||
|
|
||||||
|
def is_first(self, offset: int) -> bool:
|
||||||
|
return self.beginning == offset
|
||||||
|
|
||||||
|
def is_last(self, offset: int) -> bool:
|
||||||
|
return self.end == (offset + 1)
|
||||||
|
|
||||||
|
def split(self, offset: int, link: bool = False) -> Tuple["ControlFlow", "ControlFlow"]:
|
||||||
|
if not self.contains(offset):
|
||||||
|
raise Exception(f"This ControlFlow does not contain offset {offset}")
|
||||||
|
|
||||||
|
# First, make the second half that the first half will point to.
|
||||||
|
second = ControlFlow(
|
||||||
|
offset,
|
||||||
|
self.end,
|
||||||
|
self.next_flow,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now, make the first half that we can point to.
|
||||||
|
first = ControlFlow(
|
||||||
|
self.beginning,
|
||||||
|
offset,
|
||||||
|
[second.beginning] if link else [],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (first, second)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"ControlFlow(beginning={self.beginning}, end={self.end}, next={(', '.join(str(n) for n in self.next_flow)) or 'N/A'}"
|
||||||
|
|
||||||
|
|
||||||
|
class ByteCodeChunk:
|
||||||
|
def __init__(self, actions: List[AP2Action], next_chunk: List[int]) -> None:
|
||||||
|
self.actions = actions
|
||||||
|
self.next_chunk = next_chunk
|
||||||
|
|
||||||
|
@property
|
||||||
|
def offset(self) -> int:
|
||||||
|
return self.actions[0].offset
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
entries: List[str] = []
|
||||||
|
for action in self.actions:
|
||||||
|
entries.extend([f" {s}" for s in str(action).split(os.linesep)])
|
||||||
|
|
||||||
|
return f"ByteCodeChunk({os.linesep}{os.linesep.join(entries)}{os.linesep} Next Offsets: {', '.join(str(n) for n in self.next_chunk) or 'None'}{os.linesep})"
|
||||||
|
|
||||||
|
|
||||||
|
class ByteCodeDecompiler(VerboseOutput):
|
||||||
|
def __init__(self, bytecode: ByteCode) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.bytecode = bytecode
|
||||||
|
|
||||||
|
def __graph_control_flow(self) -> List[ByteCodeChunk]:
|
||||||
|
# Start by assuming that the whole bytecode never directs flow. This is, confusingly,
|
||||||
|
# indexed by AP2Action offset, not by actual bytecode offset, so we can avoid the
|
||||||
|
# prickly problem of opcodes that take more than one byte in the data.
|
||||||
|
flows: Dict[int, ControlFlow] = {}
|
||||||
|
end = len(self.bytecode.actions)
|
||||||
|
beginning = 0
|
||||||
|
|
||||||
|
# The end of the program.
|
||||||
|
flows[end] = ControlFlow(end, end + 1, [])
|
||||||
|
|
||||||
|
# The rest of the program.
|
||||||
|
flows[beginning] = ControlFlow(beginning, end, [end])
|
||||||
|
|
||||||
|
# Function that helps us find a flow by position.
|
||||||
|
def find(opcodeno: int) -> int:
|
||||||
|
for start, cf in flows.items():
|
||||||
|
if cf.contains(opcodeno):
|
||||||
|
return start
|
||||||
|
|
||||||
|
raise Exception(f"Offset {opcodeno} somehow not in our control flow graph!")
|
||||||
|
|
||||||
|
# Now, walk the entire bytecode, and every control flow point split the graph at that point.
|
||||||
|
for i, action in enumerate(self.bytecode.actions):
|
||||||
|
current_action = i
|
||||||
|
next_action = i + 1
|
||||||
|
|
||||||
|
if action.opcode in [AP2Action.THROW, AP2Action.RETURN]:
|
||||||
|
# This should end execution, so we should cap off the current execution
|
||||||
|
# and send it to the end.
|
||||||
|
current_action_flow = find(current_action)
|
||||||
|
next_action_flow = find(next_action)
|
||||||
|
|
||||||
|
if current_action_flow == next_action_flow:
|
||||||
|
# We need to split this on the next_action boundary.
|
||||||
|
first, second = flows[current_action_flow].split(next_action)
|
||||||
|
first.next_flow = [end]
|
||||||
|
|
||||||
|
self.vprint(f"{action} action split {flows[current_action_flow]} into {first}, {second}")
|
||||||
|
|
||||||
|
flows[current_action_flow] = first
|
||||||
|
flows[next_action] = second
|
||||||
|
|
||||||
|
else:
|
||||||
|
# This already was split in two, presumably by something
|
||||||
|
# earlier in the chain jumping to the opcode after this.
|
||||||
|
# We need to unlink the current flow from the second and
|
||||||
|
# link it to the end.
|
||||||
|
flows[current_action_flow].next_flow = [end]
|
||||||
|
|
||||||
|
self.vprint(f"{action} action repointed {flows[current_action_flow]} to end")
|
||||||
|
elif action.opcode == AP2Action.JUMP:
|
||||||
|
# Unconditional control flow redirection after this, we should split the
|
||||||
|
# section if necessary and point this section at the new offset.
|
||||||
|
# First, we need to find the jump point and make sure that its the start
|
||||||
|
# of a section.
|
||||||
|
action = cast(JumpAction, action)
|
||||||
|
for j, dest in enumerate(self.bytecode.actions):
|
||||||
|
if dest.offset == action.jump_offset:
|
||||||
|
dest_action = j
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise Exception(f"{action} jumps to an opcode that doesn't exist!")
|
||||||
|
|
||||||
|
# If the destination action flow already starts with the jump offset,
|
||||||
|
# then we're good, we just need to point our current split at this new
|
||||||
|
# offset. If it doesn't start with the jump offset, then we need to split
|
||||||
|
# that flow so we can point to the opcode directly.
|
||||||
|
dest_action_flow = find(dest_action)
|
||||||
|
if not flows[dest_action_flow].is_first(dest_action):
|
||||||
|
first, second = flows[dest_action_flow].split(dest_action, link=True)
|
||||||
|
|
||||||
|
self.vprint(f"{action} action required split of {flows[dest_action_flow]} into {first, second}")
|
||||||
|
|
||||||
|
flows[dest_action_flow] = first
|
||||||
|
flows[dest_action] = second
|
||||||
|
|
||||||
|
# Now, the second is what we want to point at in the next section.
|
||||||
|
dest_action_flow = dest_action
|
||||||
|
|
||||||
|
# Now, we must split the current flow at the point of this jump.
|
||||||
|
current_action_flow = find(current_action)
|
||||||
|
next_action_flow = find(next_action)
|
||||||
|
|
||||||
|
if current_action_flow == next_action_flow:
|
||||||
|
# We need to split this on the next_action boundary.
|
||||||
|
first, second = flows[current_action_flow].split(next_action)
|
||||||
|
first.next_flow = [dest_action_flow]
|
||||||
|
|
||||||
|
self.vprint(f"{action} action split {flows[current_action_flow]} into {first}, {second}")
|
||||||
|
|
||||||
|
flows[current_action_flow] = first
|
||||||
|
flows[next_action] = second
|
||||||
|
else:
|
||||||
|
# This already was split in two, presumably by something
|
||||||
|
# earlier in the chain jumping to the opcode after this.
|
||||||
|
# We need to unlink the current flow from the second and
|
||||||
|
# link it to the end.
|
||||||
|
flows[current_action_flow].next_flow = [dest_action_flow]
|
||||||
|
|
||||||
|
self.vprint(f"{action} action repointed {flows[current_action_flow]} to new chunk")
|
||||||
|
elif action.opcode in [AP2Action.IF, AP2Action.IF2]:
|
||||||
|
# Conditional control flow redirection after this, we should split the
|
||||||
|
# section if necessary and point this section at the new offset as well
|
||||||
|
# as the second half of the split section.
|
||||||
|
# First, we need to find the jump point and make sure that its the start
|
||||||
|
# of a section.
|
||||||
|
action = cast(IfAction, action)
|
||||||
|
for j, dest in enumerate(self.bytecode.actions):
|
||||||
|
if dest.offset == action.jump_if_true_offset:
|
||||||
|
dest_action = j
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise Exception(f"{action} conditional jumps to an opcode that doesn't exist!")
|
||||||
|
|
||||||
|
# If the destination action flow already starts with the jump offset,
|
||||||
|
# then we're good, we just need to point our current split at this new
|
||||||
|
# offset. If it doesn't start with the jump offset, then we need to split
|
||||||
|
# that flow so we can point to the opcode directly.
|
||||||
|
dest_action_flow = find(dest_action)
|
||||||
|
if not flows[dest_action_flow].is_first(dest_action):
|
||||||
|
first, second = flows[dest_action_flow].split(dest_action, link=True)
|
||||||
|
|
||||||
|
self.vprint(f"{action} action required split of {flows[dest_action_flow]} into {first, second}")
|
||||||
|
|
||||||
|
flows[dest_action_flow] = first
|
||||||
|
flows[dest_action] = second
|
||||||
|
|
||||||
|
# Now, the second is what we want to point at in the next section.
|
||||||
|
dest_action_flow = dest_action
|
||||||
|
|
||||||
|
# Now, we must split the current flow at the point of this jump.
|
||||||
|
current_action_flow = find(current_action)
|
||||||
|
next_action_flow = find(next_action)
|
||||||
|
|
||||||
|
if current_action_flow == next_action_flow:
|
||||||
|
# We need to split this on the next_action boundary.
|
||||||
|
first, second = flows[current_action_flow].split(next_action)
|
||||||
|
first.next_flow = [next_action, dest_action_flow]
|
||||||
|
|
||||||
|
self.vprint(f"{action} action split {flows[current_action_flow]} into {first}, {second}")
|
||||||
|
|
||||||
|
flows[current_action_flow] = first
|
||||||
|
flows[next_action] = second
|
||||||
|
else:
|
||||||
|
# This already was split in two, presumably by something
|
||||||
|
# earlier in the chain jumping to the opcode after this.
|
||||||
|
# We need to unlink the current flow from the second and
|
||||||
|
# link it to the end.
|
||||||
|
flows[current_action_flow].next_flow = [next_action, dest_action_flow]
|
||||||
|
|
||||||
|
self.vprint(f"{action} action repointed {flows[current_action_flow]} to new chunk")
|
||||||
|
|
||||||
|
# Finally, return chunks of contiguous execution.
|
||||||
|
chunks: List[ByteCodeChunk] = []
|
||||||
|
for start, flow in flows.items():
|
||||||
|
if start == end:
|
||||||
|
# We don't want to render out the end of the graph, it was only there to make
|
||||||
|
# the above algorithm easier.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(flow.next_flow) == 1 and flow.next_flow[0] == end:
|
||||||
|
# This flow is a termination state.
|
||||||
|
chunks.append(ByteCodeChunk(self.bytecode.actions[flow.beginning:flow.end], []))
|
||||||
|
else:
|
||||||
|
next_chunks: List[int] = []
|
||||||
|
for ano in flow.next_flow:
|
||||||
|
if ano == end:
|
||||||
|
raise Exception("Logic error!")
|
||||||
|
next_chunks.append(self.bytecode.actions[ano].offset)
|
||||||
|
chunks.append(ByteCodeChunk(self.bytecode.actions[flow.beginning:flow.end], next_chunks))
|
||||||
|
|
||||||
|
return sorted(chunks, key=lambda c: c.offset)
|
||||||
|
|
||||||
|
def decompile(self, verbose: bool = False) -> str:
|
||||||
|
with self.debugging(verbose):
|
||||||
|
return self.__decompile()
|
||||||
|
|
||||||
|
def __decompile(self) -> str:
|
||||||
|
# First, we need to construct a control flow graph.
|
||||||
|
chunks = self.__graph_control_flow()
|
||||||
|
|
||||||
|
self.vprint(chunks)
|
||||||
|
|
||||||
|
return "TODO"
|
||||||
@@ -34,13 +34,13 @@ class Shape:
|
|||||||
# Whether this is parsed.
|
# Whether this is parsed.
|
||||||
self.parsed = False
|
self.parsed = False
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'vertex_points': [p.as_dict() for p in self.vertex_points],
|
'vertex_points': [p.as_dict(*args, **kwargs) for p in self.vertex_points],
|
||||||
'tex_points': [p.as_dict() for p in self.tex_points],
|
'tex_points': [p.as_dict(*args, **kwargs) for p in self.tex_points],
|
||||||
'tex_colors': [c.as_dict() for c in self.tex_colors],
|
'tex_colors': [c.as_dict(*args, **kwargs) for c in self.tex_colors],
|
||||||
'draw_params': [d.as_dict() for d in self.draw_params],
|
'draw_params': [d.as_dict(*args, **kwargs) for d in self.draw_params],
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -205,12 +205,12 @@ class DrawParams:
|
|||||||
self.vertexes = vertexes
|
self.vertexes = vertexes
|
||||||
self.blend = blend
|
self.blend = blend
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'flags': self.flags,
|
'flags': self.flags,
|
||||||
'region': self.region,
|
'region': self.region,
|
||||||
'vertexes': self.vertexes,
|
'vertexes': self.vertexes,
|
||||||
'blend': self.blend.as_dict() if self.blend else None,
|
'blend': self.blend.as_dict(*args, **kwargs) if self.blend else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|||||||
@@ -3,9 +3,34 @@ import struct
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from .decompile import ByteCode
|
||||||
from .types import Matrix, Color, Point, Rectangle
|
from .types import Matrix, Color, Point, Rectangle
|
||||||
from .types import AP2Action, AP2Tag, AP2Property
|
from .types import (
|
||||||
from .util import TrackedCoverage, VerboseOutput, _hex
|
AP2Action,
|
||||||
|
AP2Tag,
|
||||||
|
AP2Property,
|
||||||
|
DefineFunction2Action,
|
||||||
|
InitRegisterAction,
|
||||||
|
StoreRegisterAction,
|
||||||
|
JumpAction,
|
||||||
|
WithAction,
|
||||||
|
PushAction,
|
||||||
|
AddNumVariableAction,
|
||||||
|
AddNumRegisterAction,
|
||||||
|
IfAction,
|
||||||
|
GetURL2Action,
|
||||||
|
StartDragAction,
|
||||||
|
GotoFrame2Action,
|
||||||
|
Register,
|
||||||
|
NULL,
|
||||||
|
UNDEFINED,
|
||||||
|
THIS,
|
||||||
|
ROOT,
|
||||||
|
PARENT,
|
||||||
|
CLIP,
|
||||||
|
GLOBAL,
|
||||||
|
)
|
||||||
|
from .util import TrackedCoverage, VerboseOutput
|
||||||
|
|
||||||
|
|
||||||
class NamedTagReference:
|
class NamedTagReference:
|
||||||
@@ -13,7 +38,7 @@ class NamedTagReference:
|
|||||||
self.swf = swf_name
|
self.swf = swf_name
|
||||||
self.tag = tag_name
|
self.tag = tag_name
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'swf': self.swf,
|
'swf': self.swf,
|
||||||
'tag': self.tag,
|
'tag': self.tag,
|
||||||
@@ -23,195 +48,6 @@ class NamedTagReference:
|
|||||||
return f"{self.swf}.{self.tag}"
|
return f"{self.swf}.{self.tag}"
|
||||||
|
|
||||||
|
|
||||||
class DefineFunction2Action(AP2Action):
|
|
||||||
def __init__(self, offset: int, name: Optional[str], flags: int, body: "ByteCode") -> None:
|
|
||||||
super().__init__(offset, AP2Action.DEFINE_FUNCTION2)
|
|
||||||
self.name = name
|
|
||||||
self.flags = flags
|
|
||||||
self.body = body
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
bytecode = [f" {line}" for line in str(self.body).split(os.linesep)]
|
|
||||||
action_name = AP2Action.action_to_name(self.opcode)
|
|
||||||
return os.linesep.join([
|
|
||||||
f"{self.offset}: {action_name}, Name: {self.name or '<anonymous function>'}, Flags: {hex(self.flags)}",
|
|
||||||
*bytecode,
|
|
||||||
f"END_{action_name}",
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
# A bunch of stuff for implementing PushAction
|
|
||||||
class GenericObject:
|
|
||||||
def __init__(self, name: str) -> None:
|
|
||||||
self.__name = name
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return self.__name
|
|
||||||
|
|
||||||
|
|
||||||
NULL = GenericObject('NULL')
|
|
||||||
UNDEFINED = GenericObject('UNDEFINED')
|
|
||||||
THIS = GenericObject('THIS')
|
|
||||||
ROOT = GenericObject('ROOT')
|
|
||||||
PARENT = GenericObject('PARENT')
|
|
||||||
CLIP = GenericObject('CLIP')
|
|
||||||
GLOBAL = GenericObject('GLOBAL')
|
|
||||||
|
|
||||||
|
|
||||||
class Register:
|
|
||||||
def __init__(self, no: int) -> None:
|
|
||||||
self.no = no
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"Register {self.no}"
|
|
||||||
|
|
||||||
|
|
||||||
class PushAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, objects: List[Any]) -> None:
|
|
||||||
super().__init__(offset, AP2Action.PUSH)
|
|
||||||
self.objects = objects
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
objects = [f" {repr(obj)}" for obj in self.objects]
|
|
||||||
action_name = AP2Action.action_to_name(self.opcode)
|
|
||||||
return os.linesep.join([
|
|
||||||
f"{self.offset}: {action_name}",
|
|
||||||
*objects,
|
|
||||||
f"END_{action_name}",
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class InitRegisterAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, registers: List[Register]) -> None:
|
|
||||||
super().__init__(offset, AP2Action.INIT_REGISTER)
|
|
||||||
self.registers = registers
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
registers = [f" {reg}" for reg in self.registers]
|
|
||||||
action_name = AP2Action.action_to_name(self.opcode)
|
|
||||||
return os.linesep.join([
|
|
||||||
f"{self.offset}: {action_name}",
|
|
||||||
*registers,
|
|
||||||
f"END_{action_name}",
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class StoreRegisterAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, registers: List[Register]) -> None:
|
|
||||||
super().__init__(offset, AP2Action.STORE_REGISTER)
|
|
||||||
self.registers = registers
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
registers = [f" {reg}" for reg in self.registers]
|
|
||||||
action_name = AP2Action.action_to_name(self.opcode)
|
|
||||||
return os.linesep.join([
|
|
||||||
f"{self.offset}: {action_name}",
|
|
||||||
*registers,
|
|
||||||
f"END_{action_name}",
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class IfAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, jump_if_true_offset: int) -> None:
|
|
||||||
super().__init__(offset, AP2Action.IF)
|
|
||||||
self.jump_if_true_offset = jump_if_true_offset
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Offset To Jump To If True: {self.jump_if_true_offset}"
|
|
||||||
|
|
||||||
|
|
||||||
class If2Action(AP2Action):
|
|
||||||
def __init__(self, offset: int, comparison: str, jump_if_true_offset: int) -> None:
|
|
||||||
super().__init__(offset, AP2Action.IF2)
|
|
||||||
self.comparison = comparison
|
|
||||||
self.jump_if_true_offset = jump_if_true_offset
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Comparison: {self.comparison}, Offset To Jump To If True: {self.jump_if_true_offset}"
|
|
||||||
|
|
||||||
|
|
||||||
class JumpAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, jump_offset: int) -> None:
|
|
||||||
super().__init__(offset, AP2Action.JUMP)
|
|
||||||
self.jump_offset = jump_offset
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Offset To Jump To: {self.jump_offset}"
|
|
||||||
|
|
||||||
|
|
||||||
class WithAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, unknown: bytes) -> None:
|
|
||||||
super().__init__(offset, AP2Action.WITH)
|
|
||||||
self.unknown = unknown
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Unknown: {self.unknown!r}"
|
|
||||||
|
|
||||||
|
|
||||||
class GotoFrame2Action(AP2Action):
|
|
||||||
def __init__(self, offset: int, additional_frames: int, stop: bool) -> None:
|
|
||||||
super().__init__(offset, AP2Action.GOTO_FRAME2)
|
|
||||||
self.additional_frames = additional_frames
|
|
||||||
self.stop = stop
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Additional Frames: {self.additional_frames}, Stop On Arrival: {'yes': if self.stop else 'no'}"
|
|
||||||
|
|
||||||
|
|
||||||
class AddNumVariableAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, amount_to_add: int) -> None:
|
|
||||||
super().__init__(offset, AP2Action.ADD_NUM_VARIABLE)
|
|
||||||
self.amount_to_add = amount_to_add
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Amount To Add: {self.amount_to_add}"
|
|
||||||
|
|
||||||
|
|
||||||
class AddNumRegisterAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, register: Register, amount_to_add: int) -> None:
|
|
||||||
super().__init__(offset, AP2Action.ADD_NUM_REGISTER)
|
|
||||||
self.register = register
|
|
||||||
self.amount_to_add = amount_to_add
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Register: {self.register}, Amount To Add: {self.amount_to_add}"
|
|
||||||
|
|
||||||
|
|
||||||
class GetURL2Action(AP2Action):
|
|
||||||
def __init__(self, offset: int, action: int) -> None:
|
|
||||||
super().__init__(offset, AP2Action.GET_URL2)
|
|
||||||
self.action = action
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Action: {self.action}"
|
|
||||||
|
|
||||||
|
|
||||||
class StartDragAction(AP2Action):
|
|
||||||
def __init__(self, offset: int, constrain: Optional[bool]) -> None:
|
|
||||||
super().__init__(offset, AP2Action.START_DRAG)
|
|
||||||
self.constrain = constrain
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
if self.constrain is None:
|
|
||||||
cstr = "check stack"
|
|
||||||
else:
|
|
||||||
cstr = "yes" if self.constrain else "no"
|
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Constrain Mouse: {cstr}"
|
|
||||||
|
|
||||||
|
|
||||||
class ByteCode:
|
|
||||||
# A list of bytecodes to execute.
|
|
||||||
def __init__(self, actions: List[AP2Action]) -> None:
|
|
||||||
self.actions = actions
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
entries: List[str] = []
|
|
||||||
for action in self.actions:
|
|
||||||
entries.extend([f" {s}" for s in str(action).split(os.linesep)])
|
|
||||||
|
|
||||||
return f"ByteCode({os.linesep}{os.linesep.join(entries)}{os.linesep})"
|
|
||||||
|
|
||||||
|
|
||||||
class TagPointer:
|
class TagPointer:
|
||||||
# A pointer to a tag in this SWF by Tag ID and containing an optional initialization bytecode
|
# 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.
|
# to run for this tag when it is placed/executed.
|
||||||
@@ -219,6 +55,12 @@ class TagPointer:
|
|||||||
self.id = id
|
self.id = id
|
||||||
self.init_bytecode = init_bytecode
|
self.init_bytecode = init_bytecode
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'init_bytecode': self.init_bytecode.as_dict(*args, **kwargs) if self.init_bytecode else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Frame:
|
class Frame:
|
||||||
def __init__(self, start_tag_offset: int, num_tags: int, imported_tags: List[TagPointer] = []) -> None:
|
def __init__(self, start_tag_offset: int, num_tags: int, imported_tags: List[TagPointer] = []) -> None:
|
||||||
@@ -229,19 +71,29 @@ class Frame:
|
|||||||
self.num_tags = num_tags
|
self.num_tags = num_tags
|
||||||
|
|
||||||
# A list of any imported tags that are to be placed this frame.
|
# A list of any imported tags that are to be placed this frame.
|
||||||
self.imported_tags = imported_tags
|
self.imported_tags = imported_tags or []
|
||||||
|
|
||||||
# The current tag we're processing, if any.
|
# The current tag we're processing, if any.
|
||||||
self.current_tag = 0
|
self.current_tag = 0
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'start_tag_offset': self.start_tag_offset,
|
||||||
|
'num_tags': self.num_tags,
|
||||||
|
'imported_tags': [i.as_dict(*args, **kwargs) for i in self.imported_tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Tag:
|
class Tag:
|
||||||
# Any tag that can appear in the SWF. All tags will subclass from this for their behavior.
|
# Any tag that can appear in the SWF. All tags will subclass from this for their behavior.
|
||||||
def __init__(self, id: Optional[int]) -> None:
|
def __init__(self, id: Optional[int]) -> None:
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def children(self) -> List["Tag"]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return []
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'type': self.__class__.__name__,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2ShapeTag(Tag):
|
class AP2ShapeTag(Tag):
|
||||||
@@ -251,6 +103,12 @@ class AP2ShapeTag(Tag):
|
|||||||
# The reference is the name of a shape (geo structure) that defines this primitive or sprite.
|
# The reference is the name of a shape (geo structure) that defines this primitive or sprite.
|
||||||
self.reference = reference
|
self.reference = reference
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'reference': self.reference,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2DefineFontTag(Tag):
|
class AP2DefineFontTag(Tag):
|
||||||
def __init__(self, id: int, fontname: str, xml_prefix: str, heights: List[int]) -> None:
|
def __init__(self, id: int, fontname: str, xml_prefix: str, heights: List[int]) -> None:
|
||||||
@@ -268,6 +126,14 @@ class AP2DefineFontTag(Tag):
|
|||||||
# in the texture map.
|
# in the texture map.
|
||||||
self.heights = heights
|
self.heights = heights
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'fontname': self.fontname,
|
||||||
|
'xml_prefix': self.xml_prefix,
|
||||||
|
'heights': self.heights,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2DoActionTag(Tag):
|
class AP2DoActionTag(Tag):
|
||||||
def __init__(self, bytecode: ByteCode) -> None:
|
def __init__(self, bytecode: ByteCode) -> None:
|
||||||
@@ -278,6 +144,12 @@ class AP2DoActionTag(Tag):
|
|||||||
# this tag is placed/executed.
|
# this tag is placed/executed.
|
||||||
self.bytecode = bytecode
|
self.bytecode = bytecode
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'bytecode': self.bytecode.as_dict(*args, **kwargs),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2PlaceObjectTag(Tag):
|
class AP2PlaceObjectTag(Tag):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -330,6 +202,22 @@ class AP2PlaceObjectTag(Tag):
|
|||||||
# fires.
|
# fires.
|
||||||
self.triggers = triggers
|
self.triggers = triggers
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'object_id': self.object_id,
|
||||||
|
'depth': self.depth,
|
||||||
|
'source_tag_id': self.source_tag_id,
|
||||||
|
'name': self.name,
|
||||||
|
'blend': self.blend,
|
||||||
|
'update': self.update,
|
||||||
|
'transform': self.transform.as_dict(*args, **kwargs) if self.transform is not None else None,
|
||||||
|
'rotation_offset': self.rotation_offset.as_dict(*args, **kwargs) if self.rotation_offset is not None else None,
|
||||||
|
'mult_color': self.mult_color.as_dict(*args, **kwargs) if self.mult_color is not None else None,
|
||||||
|
'add_color': self.add_color.as_dict(*args, **kwargs) if self.add_color is not None else None,
|
||||||
|
'triggers': {i: [b.as_dict(*args, **kwargs) for b in t] for (i, t) in self.triggers.items()}
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"AP2PlaceObjectTag(object_id={self.object_id}, depth={self.depth})"
|
return f"AP2PlaceObjectTag(object_id={self.object_id}, depth={self.depth})"
|
||||||
|
|
||||||
@@ -345,6 +233,13 @@ class AP2RemoveObjectTag(Tag):
|
|||||||
# The depth (level) that we should remove objects from.
|
# The depth (level) that we should remove objects from.
|
||||||
self.depth = depth
|
self.depth = depth
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'object_id': self.object_id,
|
||||||
|
'depth': self.depth,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2DefineSpriteTag(Tag):
|
class AP2DefineSpriteTag(Tag):
|
||||||
def __init__(self, id: int, tags: List[Tag], frames: List[Frame]) -> None:
|
def __init__(self, id: int, tags: List[Tag], frames: List[Frame]) -> None:
|
||||||
@@ -357,8 +252,12 @@ class AP2DefineSpriteTag(Tag):
|
|||||||
# The list of frames this SWF occupies.
|
# The list of frames this SWF occupies.
|
||||||
self.frames = frames
|
self.frames = frames
|
||||||
|
|
||||||
def children(self) -> List["Tag"]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return self.tags
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'tags': [t.as_dict(*args, **kwargs) for t in self.tags],
|
||||||
|
'frames': [f.as_dict(*args, **kwargs) for f in self.frames],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2DefineEditTextTag(Tag):
|
class AP2DefineEditTextTag(Tag):
|
||||||
@@ -381,6 +280,16 @@ class AP2DefineEditTextTag(Tag):
|
|||||||
# The default text that should be present in the control when it is initially placed/executed.
|
# The default text that should be present in the control when it is initially placed/executed.
|
||||||
self.default_text = default_text
|
self.default_text = default_text
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'font_tag_id': self.font_tag_id,
|
||||||
|
'font_height': self.font_height,
|
||||||
|
'rect': self.rect.as_dict(*args, **kwargs),
|
||||||
|
'color': self.color.as_dict(*args, **kwargs),
|
||||||
|
'default_text': self.default_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SWF(TrackedCoverage, VerboseOutput):
|
class SWF(TrackedCoverage, VerboseOutput):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -448,11 +357,19 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
|
|
||||||
print(f"Uncovered string: {hex(offset)} - {string}", file=sys.stderr)
|
print(f"Uncovered string: {hex(offset)} - {string}", file=sys.stderr)
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'data': "".join(_hex(x) for x in self.data),
|
'exported_name': self.exported_name,
|
||||||
'descramble_info': "".join(_hex(x) for x in self.descramble_info),
|
'data_version': self.data_version,
|
||||||
|
'container_version': self.container_version,
|
||||||
|
'fps': self.fps,
|
||||||
|
'color': self.color.as_dict(*args, **kwargs) if self.color is not None else None,
|
||||||
|
'location': self.location.as_dict(*args, **kwargs),
|
||||||
|
'exported_tags': self.exported_tags,
|
||||||
|
'imported_tags': {i: self.imported_tags[i].as_dict(*args, **kwargs) for i in self.imported_tags},
|
||||||
|
'tags': [t.as_dict(*args, **kwargs) for t in self.tags],
|
||||||
|
'frames': [f.as_dict(*args, **kwargs) for f in self.frames],
|
||||||
}
|
}
|
||||||
|
|
||||||
def __parse_bytecode(self, datachunk: bytes, string_offsets: List[int] = [], prefix: str = "") -> ByteCode:
|
def __parse_bytecode(self, datachunk: bytes, string_offsets: List[int] = [], prefix: str = "") -> ByteCode:
|
||||||
@@ -851,7 +768,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
offset_ptr += 3
|
offset_ptr += 3
|
||||||
|
|
||||||
self.vprint(f"{prefix} {lineno}: Offset If True: {jump_if_true_offset}")
|
self.vprint(f"{prefix} {lineno}: Offset If True: {jump_if_true_offset}")
|
||||||
actions.append(IfAction(lineno, jump_if_true_offset))
|
actions.append(IfAction(lineno, "IS TRUE", jump_if_true_offset))
|
||||||
elif opcode == AP2Action.IF2:
|
elif opcode == AP2Action.IF2:
|
||||||
if2_type, jump_if_true_offset = struct.unpack(">Bh", datachunk[(offset_ptr + 1):(offset_ptr + 4)])
|
if2_type, jump_if_true_offset = struct.unpack(">Bh", datachunk[(offset_ptr + 1):(offset_ptr + 4)])
|
||||||
jump_if_true_offset += (lineno + 4)
|
jump_if_true_offset += (lineno + 4)
|
||||||
@@ -864,7 +781,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
3: ">",
|
3: ">",
|
||||||
4: "<=",
|
4: "<=",
|
||||||
5: ">=",
|
5: ">=",
|
||||||
6: "!",
|
6: "IS FALSE",
|
||||||
7: "BITAND",
|
7: "BITAND",
|
||||||
8: "BITNOTAND",
|
8: "BITNOTAND",
|
||||||
9: "STRICT ==",
|
9: "STRICT ==",
|
||||||
@@ -874,7 +791,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
}[if2_type]
|
}[if2_type]
|
||||||
|
|
||||||
self.vprint(f"{prefix} {lineno}: {action_name} {if2_typestr}, Offset If True: {jump_if_true_offset}")
|
self.vprint(f"{prefix} {lineno}: {action_name} {if2_typestr}, Offset If True: {jump_if_true_offset}")
|
||||||
actions.append(If2Action(lineno, if2_typestr, jump_if_true_offset))
|
actions.append(IfAction(lineno, if2_typestr, jump_if_true_offset))
|
||||||
elif opcode == AP2Action.JUMP:
|
elif opcode == AP2Action.JUMP:
|
||||||
jump_offset = struct.unpack(">h", datachunk[(offset_ptr + 1):(offset_ptr + 3)])[0]
|
jump_offset = struct.unpack(">h", datachunk[(offset_ptr + 1):(offset_ptr + 3)])[0]
|
||||||
jump_offset += (lineno + 3)
|
jump_offset += (lineno + 3)
|
||||||
@@ -937,7 +854,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"Can't advance, no handler for opcode {opcode} ({hex(opcode)})!")
|
raise Exception(f"Can't advance, no handler for opcode {opcode} ({hex(opcode)})!")
|
||||||
|
|
||||||
return ByteCode(actions)
|
return ByteCode(actions, offset_ptr)
|
||||||
|
|
||||||
def __parse_tag(self, ap2_version: int, afp_version: int, ap2data: bytes, tagid: int, size: int, dataoffset: int, prefix: str = "") -> Tag:
|
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 tagid == AP2Tag.AP2_SHAPE:
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
from .generic import Matrix, Color, Point, Rectangle
|
from .generic import Matrix, Color, Point, Rectangle
|
||||||
from .ap2 import AP2Tag, AP2Action, AP2Object, AP2Pointer, AP2Property
|
from .ap2 import (
|
||||||
|
AP2Tag,
|
||||||
|
AP2Action,
|
||||||
|
AP2Object,
|
||||||
|
AP2Pointer,
|
||||||
|
AP2Property,
|
||||||
|
DefineFunction2Action,
|
||||||
|
NULL,
|
||||||
|
UNDEFINED,
|
||||||
|
THIS,
|
||||||
|
ROOT,
|
||||||
|
PARENT,
|
||||||
|
CLIP,
|
||||||
|
GLOBAL,
|
||||||
|
Register,
|
||||||
|
PushAction,
|
||||||
|
InitRegisterAction,
|
||||||
|
StoreRegisterAction,
|
||||||
|
IfAction,
|
||||||
|
JumpAction,
|
||||||
|
WithAction,
|
||||||
|
GotoFrame2Action,
|
||||||
|
AddNumVariableAction,
|
||||||
|
AddNumRegisterAction,
|
||||||
|
GetURL2Action,
|
||||||
|
StartDragAction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -12,4 +38,24 @@ __all__ = [
|
|||||||
'AP2Object',
|
'AP2Object',
|
||||||
'AP2Pointer',
|
'AP2Pointer',
|
||||||
'AP2Property',
|
'AP2Property',
|
||||||
|
'DefineFunction2Action',
|
||||||
|
'NULL',
|
||||||
|
'UNDEFINED',
|
||||||
|
'THIS',
|
||||||
|
'ROOT',
|
||||||
|
'PARENT',
|
||||||
|
'CLIP',
|
||||||
|
'GLOBAL',
|
||||||
|
'Register',
|
||||||
|
'PushAction',
|
||||||
|
'InitRegisterAction',
|
||||||
|
'StoreRegisterAction',
|
||||||
|
'IfAction',
|
||||||
|
'JumpAction',
|
||||||
|
'WithAction',
|
||||||
|
'GotoFrame2Action',
|
||||||
|
'AddNumVariableAction',
|
||||||
|
'AddNumRegisterAction',
|
||||||
|
'GetURL2Action',
|
||||||
|
'StartDragAction',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
from typing import Dict, List, Set, Tuple
|
import os
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Set, Tuple, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# This is a circular dependency otherwise.
|
||||||
|
from ..decompile import ByteCode
|
||||||
|
|
||||||
|
|
||||||
class AP2Tag:
|
class AP2Tag:
|
||||||
@@ -574,10 +579,263 @@ class AP2Action:
|
|||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.opcode = opcode
|
self.opcode = opcode
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'offset': self.offset,
|
||||||
|
'action': AP2Action.action_to_name(self.opcode),
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}"
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}"
|
||||||
|
|
||||||
|
|
||||||
|
class DefineFunction2Action(AP2Action):
|
||||||
|
def __init__(self, offset: int, name: Optional[str], flags: int, body: "ByteCode") -> None:
|
||||||
|
super().__init__(offset, AP2Action.DEFINE_FUNCTION2)
|
||||||
|
self.name = name
|
||||||
|
self.flags = flags
|
||||||
|
self.body = body
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'name': self.name,
|
||||||
|
'flags': self.flags,
|
||||||
|
'body': self.body.as_dict(*args, **kwargs),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
bytecode = [f" {line}" for line in str(self.body).split(os.linesep)]
|
||||||
|
action_name = AP2Action.action_to_name(self.opcode)
|
||||||
|
return os.linesep.join([
|
||||||
|
f"{self.offset}: {action_name}, Name: {self.name or '<anonymous function>'}, Flags: {hex(self.flags)}",
|
||||||
|
*bytecode,
|
||||||
|
f"END_{action_name}",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
# A bunch of stuff for implementing PushAction
|
||||||
|
class GenericObject:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.__name = name
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
|
||||||
|
NULL = GenericObject('NULL')
|
||||||
|
UNDEFINED = GenericObject('UNDEFINED')
|
||||||
|
THIS = GenericObject('THIS')
|
||||||
|
ROOT = GenericObject('ROOT')
|
||||||
|
PARENT = GenericObject('PARENT')
|
||||||
|
CLIP = GenericObject('CLIP')
|
||||||
|
GLOBAL = GenericObject('GLOBAL')
|
||||||
|
|
||||||
|
|
||||||
|
class Register:
|
||||||
|
def __init__(self, no: int) -> None:
|
||||||
|
self.no = no
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Register {self.no}"
|
||||||
|
|
||||||
|
|
||||||
|
class PushAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, objects: List[Any]) -> None:
|
||||||
|
super().__init__(offset, AP2Action.PUSH)
|
||||||
|
self.objects = objects
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
# TODO: We need to do better than this when exporting objects,
|
||||||
|
# we should preserve their type.
|
||||||
|
'objects': [repr(o) for o in self.objects],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
objects = [f" {repr(obj)}" for obj in self.objects]
|
||||||
|
action_name = AP2Action.action_to_name(self.opcode)
|
||||||
|
return os.linesep.join([
|
||||||
|
f"{self.offset}: {action_name}",
|
||||||
|
*objects,
|
||||||
|
f"END_{action_name}",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class InitRegisterAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, registers: List[Register]) -> None:
|
||||||
|
super().__init__(offset, AP2Action.INIT_REGISTER)
|
||||||
|
self.registers = registers
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'registers': [r.no for r in self.registers],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
registers = [f" {reg}" for reg in self.registers]
|
||||||
|
action_name = AP2Action.action_to_name(self.opcode)
|
||||||
|
return os.linesep.join([
|
||||||
|
f"{self.offset}: {action_name}",
|
||||||
|
*registers,
|
||||||
|
f"END_{action_name}",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class StoreRegisterAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, registers: List[Register]) -> None:
|
||||||
|
super().__init__(offset, AP2Action.STORE_REGISTER)
|
||||||
|
self.registers = registers
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'registers': [r.no for r in self.registers],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
registers = [f" {reg}" for reg in self.registers]
|
||||||
|
action_name = AP2Action.action_to_name(self.opcode)
|
||||||
|
return os.linesep.join([
|
||||||
|
f"{self.offset}: {action_name}",
|
||||||
|
*registers,
|
||||||
|
f"END_{action_name}",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class IfAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, comparison: str, jump_if_true_offset: int) -> None:
|
||||||
|
super().__init__(offset, AP2Action.IF2)
|
||||||
|
self.comparison = comparison
|
||||||
|
self.jump_if_true_offset = jump_if_true_offset
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'jump_if_true_offset': self.jump_if_true_offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Comparison: {self.comparison}, Offset To Jump To If True: {self.jump_if_true_offset}"
|
||||||
|
|
||||||
|
|
||||||
|
class JumpAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, jump_offset: int) -> None:
|
||||||
|
super().__init__(offset, AP2Action.JUMP)
|
||||||
|
self.jump_offset = jump_offset
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'jump_offset': self.jump_offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Offset To Jump To: {self.jump_offset}"
|
||||||
|
|
||||||
|
|
||||||
|
class WithAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, unknown: bytes) -> None:
|
||||||
|
super().__init__(offset, AP2Action.WITH)
|
||||||
|
self.unknown = unknown
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
# TODO: We need to do better than this, so I guess it comes down to having
|
||||||
|
# a better idea how WITH works.
|
||||||
|
'unknown': str(self.unknown),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Unknown: {self.unknown!r}"
|
||||||
|
|
||||||
|
|
||||||
|
class GotoFrame2Action(AP2Action):
|
||||||
|
def __init__(self, offset: int, additional_frames: int, stop: bool) -> None:
|
||||||
|
super().__init__(offset, AP2Action.GOTO_FRAME2)
|
||||||
|
self.additional_frames = additional_frames
|
||||||
|
self.stop = stop
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'additiona_frames': self.additional_frames,
|
||||||
|
'stop': self.stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Additional Frames: {self.additional_frames}, Stop On Arrival: {'yes' if self.stop else 'no'}"
|
||||||
|
|
||||||
|
|
||||||
|
class AddNumVariableAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, amount_to_add: int) -> None:
|
||||||
|
super().__init__(offset, AP2Action.ADD_NUM_VARIABLE)
|
||||||
|
self.amount_to_add = amount_to_add
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'amount_to_add': self.amount_to_add,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Amount To Add: {self.amount_to_add}"
|
||||||
|
|
||||||
|
|
||||||
|
class AddNumRegisterAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, register: Register, amount_to_add: int) -> None:
|
||||||
|
super().__init__(offset, AP2Action.ADD_NUM_REGISTER)
|
||||||
|
self.register = register
|
||||||
|
self.amount_to_add = amount_to_add
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'register': self.register.no,
|
||||||
|
'amount_to_add': self.amount_to_add,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Register: {self.register}, Amount To Add: {self.amount_to_add}"
|
||||||
|
|
||||||
|
|
||||||
|
class GetURL2Action(AP2Action):
|
||||||
|
def __init__(self, offset: int, action: int) -> None:
|
||||||
|
super().__init__(offset, AP2Action.GET_URL2)
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'action': self.action,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Action: {self.action}"
|
||||||
|
|
||||||
|
|
||||||
|
class StartDragAction(AP2Action):
|
||||||
|
def __init__(self, offset: int, constrain: Optional[bool]) -> None:
|
||||||
|
super().__init__(offset, AP2Action.START_DRAG)
|
||||||
|
self.constrain = constrain
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
**super().as_dict(*args, **kwargs),
|
||||||
|
'constrain': self.constrain,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
if self.constrain is None:
|
||||||
|
cstr = "check stack"
|
||||||
|
else:
|
||||||
|
cstr = "yes" if self.constrain else "no"
|
||||||
|
return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Constrain Mouse: {cstr}"
|
||||||
|
|
||||||
|
|
||||||
class AP2Object:
|
class AP2Object:
|
||||||
UNDEFINED = 0x0
|
UNDEFINED = 0x0
|
||||||
NAN = 0x1
|
NAN = 0x1
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Color:
|
|||||||
self.b = b
|
self.b = b
|
||||||
self.a = a
|
self.a = a
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'r': self.r,
|
'r': self.r,
|
||||||
'g': self.g,
|
'g': self.g,
|
||||||
@@ -37,7 +37,7 @@ class Point:
|
|||||||
def identity() -> "Point":
|
def identity() -> "Point":
|
||||||
return Point(0.0, 0.0)
|
return Point(0.0, 0.0)
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'x': self.x,
|
'x': self.x,
|
||||||
'y': self.y,
|
'y': self.y,
|
||||||
@@ -67,7 +67,7 @@ class Rectangle:
|
|||||||
self.bottom = bottom
|
self.bottom = bottom
|
||||||
self.right = right
|
self.right = right
|
||||||
|
|
||||||
def as_dict(self) -> Dict[str, Any]:
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'left': self.left,
|
'left': self.left,
|
||||||
'top': self.top,
|
'top': self.top,
|
||||||
@@ -104,6 +104,16 @@ class Matrix:
|
|||||||
def identity() -> "Matrix":
|
def identity() -> "Matrix":
|
||||||
return Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
return Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'a': self.a,
|
||||||
|
'b': self.b,
|
||||||
|
'c': self.c,
|
||||||
|
'd': self.d,
|
||||||
|
'tx': self.tx,
|
||||||
|
'ty': self.ty,
|
||||||
|
}
|
||||||
|
|
||||||
def multiply_point(self, point: Point) -> Point:
|
def multiply_point(self, point: Point) -> Point:
|
||||||
return Point(
|
return Point(
|
||||||
x=(self.a * point.x) + (self.c * point.y) + self.tx,
|
x=(self.a * point.x) + (self.c * point.y) + self.tx,
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ def main() -> int:
|
|||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
help="The file to print",
|
help="The file to print",
|
||||||
)
|
)
|
||||||
|
print_parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--decompile-bytecode",
|
||||||
|
action="store_true",
|
||||||
|
help="Attempt to decompile and print bytecode instead of printing the raw representation.",
|
||||||
|
)
|
||||||
print_parser.add_argument(
|
print_parser.add_argument(
|
||||||
"-v",
|
"-v",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
@@ -119,6 +125,12 @@ def main() -> int:
|
|||||||
metavar="BSIFILE",
|
metavar="BSIFILE",
|
||||||
help="The BSI file to parse",
|
help="The BSI file to parse",
|
||||||
)
|
)
|
||||||
|
parseafp_parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--decompile-bytecode",
|
||||||
|
action="store_true",
|
||||||
|
help="Attempt to decompile and print bytecode instead of printing the raw representation.",
|
||||||
|
)
|
||||||
parseafp_parser.add_argument(
|
parseafp_parser.add_argument(
|
||||||
"-v",
|
"-v",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
@@ -427,7 +439,7 @@ def main() -> int:
|
|||||||
afpfile = TXP2File(bfp.read(), verbose=args.verbose)
|
afpfile = TXP2File(bfp.read(), verbose=args.verbose)
|
||||||
|
|
||||||
# Now, print it
|
# Now, print it
|
||||||
print(json.dumps(afpfile.as_dict(), sort_keys=True, indent=4))
|
print(json.dumps(afpfile.as_dict(decompile_bytecode=args.decompile_bytecode), sort_keys=True, indent=4))
|
||||||
|
|
||||||
if args.action == "parseafp":
|
if args.action == "parseafp":
|
||||||
# First, load the AFP and BSI files
|
# First, load the AFP and BSI files
|
||||||
@@ -437,7 +449,7 @@ def main() -> int:
|
|||||||
|
|
||||||
# Now, print it
|
# Now, print it
|
||||||
swf.parse(verbose=args.verbose)
|
swf.parse(verbose=args.verbose)
|
||||||
print(json.dumps(swf.as_dict(), sort_keys=True, indent=4))
|
print(json.dumps(swf.as_dict(decompile_bytecode=args.decompile_bytecode), sort_keys=True, indent=4))
|
||||||
|
|
||||||
if args.action == "parsegeo":
|
if args.action == "parsegeo":
|
||||||
# First, load the AFP and BSI files
|
# First, load the AFP and BSI files
|
||||||
|
|||||||
Reference in New Issue
Block a user