pyflashkit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flashkit/__init__.py +54 -0
- flashkit/abc/__init__.py +79 -0
- flashkit/abc/builder.py +847 -0
- flashkit/abc/constants.py +198 -0
- flashkit/abc/disasm.py +364 -0
- flashkit/abc/parser.py +434 -0
- flashkit/abc/types.py +275 -0
- flashkit/abc/writer.py +230 -0
- flashkit/analysis/__init__.py +28 -0
- flashkit/analysis/call_graph.py +317 -0
- flashkit/analysis/inheritance.py +267 -0
- flashkit/analysis/references.py +371 -0
- flashkit/analysis/strings.py +299 -0
- flashkit/cli/__init__.py +75 -0
- flashkit/cli/_util.py +52 -0
- flashkit/cli/build.py +36 -0
- flashkit/cli/callees.py +30 -0
- flashkit/cli/callers.py +30 -0
- flashkit/cli/class_cmd.py +83 -0
- flashkit/cli/classes.py +71 -0
- flashkit/cli/disasm.py +77 -0
- flashkit/cli/extract.py +36 -0
- flashkit/cli/info.py +41 -0
- flashkit/cli/packages.py +30 -0
- flashkit/cli/refs.py +31 -0
- flashkit/cli/strings.py +58 -0
- flashkit/cli/tags.py +32 -0
- flashkit/cli/tree.py +52 -0
- flashkit/errors.py +33 -0
- flashkit/info/__init__.py +31 -0
- flashkit/info/class_info.py +176 -0
- flashkit/info/member_info.py +275 -0
- flashkit/info/package_info.py +60 -0
- flashkit/search/__init__.py +16 -0
- flashkit/search/search.py +456 -0
- flashkit/swf/__init__.py +66 -0
- flashkit/swf/builder.py +283 -0
- flashkit/swf/parser.py +164 -0
- flashkit/swf/tags.py +120 -0
- flashkit/workspace/__init__.py +20 -0
- flashkit/workspace/resource.py +189 -0
- flashkit/workspace/workspace.py +232 -0
- pyflashkit-1.0.0.dist-info/METADATA +281 -0
- pyflashkit-1.0.0.dist-info/RECORD +48 -0
- pyflashkit-1.0.0.dist-info/WHEEL +5 -0
- pyflashkit-1.0.0.dist-info/entry_points.txt +2 -0
- pyflashkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyflashkit-1.0.0.dist-info/top_level.txt +1 -0
flashkit/abc/writer.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ABC bytecode serializer.
|
|
3
|
+
|
|
4
|
+
Serializes an ``AbcFile`` structure back to raw ABC binary data.
|
|
5
|
+
The output is byte-for-byte identical to the original input when
|
|
6
|
+
no modifications have been made (round-trip fidelity).
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from flashkit.abc.parser import parse_abc
|
|
11
|
+
from flashkit.abc.writer import serialize_abc
|
|
12
|
+
|
|
13
|
+
abc = parse_abc(raw_bytes)
|
|
14
|
+
# ... modify abc ...
|
|
15
|
+
output = serialize_abc(abc)
|
|
16
|
+
|
|
17
|
+
Reference: Adobe AVM2 Overview, Chapter 4 (abc file format).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import struct
|
|
23
|
+
|
|
24
|
+
from ..errors import SerializeError
|
|
25
|
+
from .types import AbcFile, TraitInfo
|
|
26
|
+
from .parser import write_u30, write_s32
|
|
27
|
+
from .constants import (
|
|
28
|
+
CONSTANT_QName, CONSTANT_QNameA,
|
|
29
|
+
CONSTANT_RTQName, CONSTANT_RTQNameA,
|
|
30
|
+
CONSTANT_RTQNameL, CONSTANT_RTQNameLA,
|
|
31
|
+
CONSTANT_Multiname, CONSTANT_MultinameA,
|
|
32
|
+
CONSTANT_MultinameL, CONSTANT_MultinameLA,
|
|
33
|
+
CONSTANT_TypeName,
|
|
34
|
+
METHOD_HasOptional, METHOD_HasParamNames,
|
|
35
|
+
INSTANCE_ProtectedNs,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write_traits(traits: list[TraitInfo]) -> bytes:
|
|
40
|
+
"""Serialize a trait list using the raw binary data stored during parse."""
|
|
41
|
+
out = write_u30(len(traits))
|
|
42
|
+
for t in traits:
|
|
43
|
+
out += t.data
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def serialize_abc(abc: AbcFile) -> bytes:
|
|
48
|
+
"""Serialize an AbcFile back to raw ABC bytecode.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
abc: The AbcFile to serialize.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Raw ABC bytecode bytes ready to embed in a DoABC/DoABC2 tag.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
SerializeError: If the AbcFile structure is invalid.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
return _serialize_abc_inner(abc)
|
|
61
|
+
except SerializeError:
|
|
62
|
+
raise
|
|
63
|
+
except (IndexError, struct.error, ValueError, TypeError,
|
|
64
|
+
AttributeError) as e:
|
|
65
|
+
raise SerializeError(f"Failed to serialize ABC: {e}") from e
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _serialize_abc_inner(abc: AbcFile) -> bytes:
|
|
69
|
+
"""Internal serializer (no error wrapping)."""
|
|
70
|
+
out = bytearray()
|
|
71
|
+
out += struct.pack("<HH", abc.minor_version, abc.major_version)
|
|
72
|
+
|
|
73
|
+
# ── Constant pool ────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
# Integers (count 0 means empty pool — only the implicit default entry)
|
|
76
|
+
# Use raw LEB128 bytes when available for round-trip fidelity, since
|
|
77
|
+
# the AVM2 spec allows non-minimal s32 encodings.
|
|
78
|
+
int_extra = abc.int_pool[1:]
|
|
79
|
+
int_raw = abc._int_pool_raw[1:] if len(abc._int_pool_raw) > 1 else []
|
|
80
|
+
out += write_u30(len(int_extra) + 1 if int_extra else 0)
|
|
81
|
+
for i, v in enumerate(int_extra):
|
|
82
|
+
if i < len(int_raw) and int_raw[i]:
|
|
83
|
+
out += int_raw[i]
|
|
84
|
+
else:
|
|
85
|
+
out += write_s32(v)
|
|
86
|
+
|
|
87
|
+
# Unsigned integers
|
|
88
|
+
# Use raw bytes for round-trip fidelity (AVM2 uint values can exceed
|
|
89
|
+
# 30 bits despite the spec calling the encoding "u30").
|
|
90
|
+
uint_extra = abc.uint_pool[1:]
|
|
91
|
+
uint_raw = abc._uint_pool_raw[1:] if len(abc._uint_pool_raw) > 1 else []
|
|
92
|
+
out += write_u30(len(uint_extra) + 1 if uint_extra else 0)
|
|
93
|
+
for i, v in enumerate(uint_extra):
|
|
94
|
+
if i < len(uint_raw) and uint_raw[i]:
|
|
95
|
+
out += uint_raw[i]
|
|
96
|
+
else:
|
|
97
|
+
out += write_u30(v)
|
|
98
|
+
|
|
99
|
+
# Doubles
|
|
100
|
+
dbl_extra = abc.double_pool[1:]
|
|
101
|
+
out += write_u30(len(dbl_extra) + 1 if dbl_extra else 0)
|
|
102
|
+
for v in dbl_extra:
|
|
103
|
+
out += struct.pack("<d", v)
|
|
104
|
+
|
|
105
|
+
# Strings
|
|
106
|
+
str_extra = abc.string_pool[1:]
|
|
107
|
+
out += write_u30(len(str_extra) + 1 if str_extra else 0)
|
|
108
|
+
for s in str_extra:
|
|
109
|
+
encoded = s.encode("utf-8")
|
|
110
|
+
out += write_u30(len(encoded))
|
|
111
|
+
out += encoded
|
|
112
|
+
|
|
113
|
+
# Namespaces
|
|
114
|
+
ns_extra = abc.namespace_pool[1:]
|
|
115
|
+
out += write_u30(len(ns_extra) + 1 if ns_extra else 0)
|
|
116
|
+
for ns in ns_extra:
|
|
117
|
+
out += bytes([ns.kind])
|
|
118
|
+
out += write_u30(ns.name)
|
|
119
|
+
|
|
120
|
+
# Namespace sets
|
|
121
|
+
nss_extra = abc.ns_set_pool[1:]
|
|
122
|
+
out += write_u30(len(nss_extra) + 1 if nss_extra else 0)
|
|
123
|
+
for nss in nss_extra:
|
|
124
|
+
out += write_u30(len(nss.namespaces))
|
|
125
|
+
for ns in nss.namespaces:
|
|
126
|
+
out += write_u30(ns)
|
|
127
|
+
|
|
128
|
+
# Multinames
|
|
129
|
+
mn_extra = abc.multiname_pool[1:]
|
|
130
|
+
out += write_u30(len(mn_extra) + 1 if mn_extra else 0)
|
|
131
|
+
for mn in mn_extra:
|
|
132
|
+
out += bytes([mn.kind])
|
|
133
|
+
if mn.kind in (CONSTANT_QName, CONSTANT_QNameA):
|
|
134
|
+
out += write_u30(mn.ns)
|
|
135
|
+
out += write_u30(mn.name)
|
|
136
|
+
elif mn.kind in (CONSTANT_RTQName, CONSTANT_RTQNameA):
|
|
137
|
+
out += write_u30(mn.name)
|
|
138
|
+
elif mn.kind in (CONSTANT_RTQNameL, CONSTANT_RTQNameLA):
|
|
139
|
+
pass
|
|
140
|
+
elif mn.kind in (CONSTANT_Multiname, CONSTANT_MultinameA):
|
|
141
|
+
out += write_u30(mn.name)
|
|
142
|
+
out += write_u30(mn.ns_set)
|
|
143
|
+
elif mn.kind in (CONSTANT_MultinameL, CONSTANT_MultinameLA):
|
|
144
|
+
out += write_u30(mn.ns_set)
|
|
145
|
+
elif mn.kind == CONSTANT_TypeName:
|
|
146
|
+
out += write_u30(mn.ns) # base type multiname index
|
|
147
|
+
out += write_u30(mn.name) # parameter count
|
|
148
|
+
out += mn.data # pre-serialized parameter u30s
|
|
149
|
+
else:
|
|
150
|
+
raise SerializeError(
|
|
151
|
+
f"Unknown multiname kind 0x{mn.kind:02X}")
|
|
152
|
+
|
|
153
|
+
# ── Methods ──────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
out += write_u30(len(abc.methods))
|
|
156
|
+
for mi in abc.methods:
|
|
157
|
+
out += write_u30(mi.param_count)
|
|
158
|
+
out += write_u30(mi.return_type)
|
|
159
|
+
for pt in mi.param_types:
|
|
160
|
+
out += write_u30(pt)
|
|
161
|
+
out += write_u30(mi.name)
|
|
162
|
+
out += bytes([mi.flags])
|
|
163
|
+
|
|
164
|
+
if mi.flags & METHOD_HasOptional:
|
|
165
|
+
out += write_u30(len(mi.options))
|
|
166
|
+
for val, vkind in mi.options:
|
|
167
|
+
out += write_u30(val)
|
|
168
|
+
out += bytes([vkind])
|
|
169
|
+
|
|
170
|
+
if mi.flags & METHOD_HasParamNames:
|
|
171
|
+
for pn in mi.param_names:
|
|
172
|
+
out += write_u30(pn)
|
|
173
|
+
|
|
174
|
+
# ── Metadata ─────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
out += write_u30(len(abc.metadata))
|
|
177
|
+
for md in abc.metadata:
|
|
178
|
+
out += write_u30(md.name)
|
|
179
|
+
out += write_u30(len(md.items))
|
|
180
|
+
for k, v in md.items:
|
|
181
|
+
out += write_u30(k)
|
|
182
|
+
out += write_u30(v)
|
|
183
|
+
|
|
184
|
+
# ── Instances + Classes ──────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
out += write_u30(len(abc.instances))
|
|
187
|
+
for inst in abc.instances:
|
|
188
|
+
out += write_u30(inst.name)
|
|
189
|
+
out += write_u30(inst.super_name)
|
|
190
|
+
out += bytes([inst.flags])
|
|
191
|
+
if inst.flags & INSTANCE_ProtectedNs:
|
|
192
|
+
out += write_u30(inst.protectedNs)
|
|
193
|
+
out += write_u30(len(inst.interfaces))
|
|
194
|
+
for ifc in inst.interfaces:
|
|
195
|
+
out += write_u30(ifc)
|
|
196
|
+
out += write_u30(inst.iinit)
|
|
197
|
+
out += _write_traits(inst.traits)
|
|
198
|
+
|
|
199
|
+
for ci in abc.classes:
|
|
200
|
+
out += write_u30(ci.cinit)
|
|
201
|
+
out += _write_traits(ci.traits)
|
|
202
|
+
|
|
203
|
+
# ── Scripts ──────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
out += write_u30(len(abc.scripts))
|
|
206
|
+
for si in abc.scripts:
|
|
207
|
+
out += write_u30(si.init)
|
|
208
|
+
out += _write_traits(si.traits)
|
|
209
|
+
|
|
210
|
+
# ── Method bodies ────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
out += write_u30(len(abc.method_bodies))
|
|
213
|
+
for mb in abc.method_bodies:
|
|
214
|
+
out += write_u30(mb.method)
|
|
215
|
+
out += write_u30(mb.max_stack)
|
|
216
|
+
out += write_u30(mb.local_count)
|
|
217
|
+
out += write_u30(mb.init_scope_depth)
|
|
218
|
+
out += write_u30(mb.max_scope_depth)
|
|
219
|
+
out += write_u30(len(mb.code))
|
|
220
|
+
out += mb.code
|
|
221
|
+
out += write_u30(len(mb.exceptions))
|
|
222
|
+
for ei in mb.exceptions:
|
|
223
|
+
out += write_u30(ei.from_offset)
|
|
224
|
+
out += write_u30(ei.to_offset)
|
|
225
|
+
out += write_u30(ei.target)
|
|
226
|
+
out += write_u30(ei.exc_type)
|
|
227
|
+
out += write_u30(ei.var_name)
|
|
228
|
+
out += _write_traits(mb.traits)
|
|
229
|
+
|
|
230
|
+
return bytes(out)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analysis services for ABC content.
|
|
3
|
+
|
|
4
|
+
This package provides graph-based and index-based analysis of the loaded
|
|
5
|
+
ABC bytecode. Each module builds a specific data structure from the parsed
|
|
6
|
+
ABC data that enables efficient queries.
|
|
7
|
+
|
|
8
|
+
Modules:
|
|
9
|
+
inheritance: InheritanceGraph — class hierarchy (parent/child/interface).
|
|
10
|
+
call_graph: CallGraph — method-to-method call edges from bytecode.
|
|
11
|
+
references: ReferenceIndex — cross-references (field types, instantiations, imports).
|
|
12
|
+
strings: StringIndex — string constant search and classification.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .inheritance import InheritanceGraph
|
|
16
|
+
from .call_graph import CallGraph, CallEdge
|
|
17
|
+
from .references import ReferenceIndex, Reference
|
|
18
|
+
from .strings import StringIndex, StringUsage
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"InheritanceGraph",
|
|
22
|
+
"CallGraph",
|
|
23
|
+
"CallEdge",
|
|
24
|
+
"ReferenceIndex",
|
|
25
|
+
"Reference",
|
|
26
|
+
"StringIndex",
|
|
27
|
+
"StringUsage",
|
|
28
|
+
]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Call graph extraction from method body bytecode.
|
|
3
|
+
|
|
4
|
+
Scans AVM2 bytecode instructions in MethodBodyInfo.code to build a
|
|
5
|
+
graph of method-to-method call edges. Each edge records the calling
|
|
6
|
+
method, target multiname, opcode type, and bytecode offset.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from flashkit.workspace import Workspace
|
|
11
|
+
from flashkit.analysis.call_graph import CallGraph
|
|
12
|
+
|
|
13
|
+
ws = Workspace()
|
|
14
|
+
ws.load_swf("application.swf")
|
|
15
|
+
graph = CallGraph.from_workspace(ws)
|
|
16
|
+
|
|
17
|
+
callers = graph.get_callers("doSomething")
|
|
18
|
+
callees = graph.get_callees("MyClass.init")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
|
|
26
|
+
from ..abc.types import AbcFile
|
|
27
|
+
from ..abc.disasm import decode_instructions
|
|
28
|
+
from ..abc.constants import (
|
|
29
|
+
OP_callproperty, OP_callpropvoid, OP_constructprop,
|
|
30
|
+
OP_getproperty, OP_setproperty, OP_initproperty,
|
|
31
|
+
OP_getlex, OP_findpropstrict, OP_newclass,
|
|
32
|
+
)
|
|
33
|
+
from ..info.member_info import resolve_multiname, build_method_body_map
|
|
34
|
+
from ..info.class_info import ClassInfo
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Opcode categories for edges
|
|
38
|
+
CALL_OPS = {OP_callproperty, OP_callpropvoid}
|
|
39
|
+
CONSTRUCT_OPS = {OP_constructprop}
|
|
40
|
+
PROPERTY_READ_OPS = {OP_getproperty, OP_getlex, OP_findpropstrict}
|
|
41
|
+
PROPERTY_WRITE_OPS = {OP_setproperty, OP_initproperty}
|
|
42
|
+
CLASS_OPS = {OP_newclass}
|
|
43
|
+
|
|
44
|
+
# All opcodes that reference a multiname in their first operand
|
|
45
|
+
_MULTINAME_OPS = (
|
|
46
|
+
CALL_OPS | CONSTRUCT_OPS | PROPERTY_READ_OPS
|
|
47
|
+
| PROPERTY_WRITE_OPS | CLASS_OPS
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class CallEdge:
|
|
53
|
+
"""A single call/reference edge in the graph.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
caller: Qualified name of the calling method (``"Class.method"``).
|
|
57
|
+
caller_method_index: Method index in the AbcFile.
|
|
58
|
+
target: Target multiname string (the called/referenced name).
|
|
59
|
+
opcode: The opcode that generated this edge.
|
|
60
|
+
mnemonic: Human-readable opcode name.
|
|
61
|
+
offset: Bytecode offset within the method body.
|
|
62
|
+
edge_type: Category string: ``"call"``, ``"construct"``, ``"read"``,
|
|
63
|
+
``"write"``, or ``"class"``.
|
|
64
|
+
"""
|
|
65
|
+
caller: str
|
|
66
|
+
caller_method_index: int
|
|
67
|
+
target: str
|
|
68
|
+
opcode: int
|
|
69
|
+
mnemonic: str
|
|
70
|
+
offset: int
|
|
71
|
+
edge_type: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _classify_op(opcode: int) -> str:
|
|
75
|
+
"""Classify an opcode into an edge type string."""
|
|
76
|
+
if opcode in CALL_OPS:
|
|
77
|
+
return "call"
|
|
78
|
+
elif opcode in CONSTRUCT_OPS:
|
|
79
|
+
return "construct"
|
|
80
|
+
elif opcode in PROPERTY_READ_OPS:
|
|
81
|
+
return "read"
|
|
82
|
+
elif opcode in PROPERTY_WRITE_OPS:
|
|
83
|
+
return "write"
|
|
84
|
+
elif opcode in CLASS_OPS:
|
|
85
|
+
return "class"
|
|
86
|
+
return "unknown"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class CallGraph:
|
|
91
|
+
"""Graph of method call and reference edges extracted from bytecode.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
edges: All edges in the graph.
|
|
95
|
+
callers_index: Map of target name → list of edges calling it.
|
|
96
|
+
callees_index: Map of caller name → list of edges it produces.
|
|
97
|
+
"""
|
|
98
|
+
edges: list[CallEdge] = field(default_factory=list)
|
|
99
|
+
callers_index: dict[str, list[CallEdge]] = field(
|
|
100
|
+
default_factory=lambda: defaultdict(list))
|
|
101
|
+
callees_index: dict[str, list[CallEdge]] = field(
|
|
102
|
+
default_factory=lambda: defaultdict(list))
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_workspace(cls, workspace: object) -> CallGraph:
|
|
106
|
+
"""Build a CallGraph from a Workspace.
|
|
107
|
+
|
|
108
|
+
Iterates all ABC blocks and their method bodies, decodes
|
|
109
|
+
instructions, and collects edges for multiname-referencing opcodes.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
workspace: A Workspace instance (with .abc_blocks and .classes).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Populated CallGraph.
|
|
116
|
+
"""
|
|
117
|
+
graph = cls()
|
|
118
|
+
|
|
119
|
+
# Build a map from method_index → class_name.method_name
|
|
120
|
+
# for all classes in the workspace
|
|
121
|
+
from ..workspace.workspace import Workspace
|
|
122
|
+
ws: Workspace = workspace # type: ignore[assignment]
|
|
123
|
+
|
|
124
|
+
for abc in ws.abc_blocks:
|
|
125
|
+
method_name_map = _build_method_name_map(abc, ws.classes)
|
|
126
|
+
method_body_map = build_method_body_map(abc)
|
|
127
|
+
|
|
128
|
+
for body in abc.method_bodies:
|
|
129
|
+
caller_name = method_name_map.get(
|
|
130
|
+
body.method, f"method_{body.method}")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
instructions = decode_instructions(body.code)
|
|
134
|
+
except Exception:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
for instr in instructions:
|
|
138
|
+
if instr.opcode in _MULTINAME_OPS and instr.operands:
|
|
139
|
+
mn_index = instr.operands[0]
|
|
140
|
+
target = resolve_multiname(abc, mn_index)
|
|
141
|
+
if target == "*" or target.startswith("multiname["):
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
edge = CallEdge(
|
|
145
|
+
caller=caller_name,
|
|
146
|
+
caller_method_index=body.method,
|
|
147
|
+
target=target,
|
|
148
|
+
opcode=instr.opcode,
|
|
149
|
+
mnemonic=instr.mnemonic,
|
|
150
|
+
offset=instr.offset,
|
|
151
|
+
edge_type=_classify_op(instr.opcode),
|
|
152
|
+
)
|
|
153
|
+
graph.edges.append(edge)
|
|
154
|
+
graph.callers_index[target].append(edge)
|
|
155
|
+
graph.callees_index[caller_name].append(edge)
|
|
156
|
+
|
|
157
|
+
return graph
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_abc(cls, abc: AbcFile,
|
|
161
|
+
classes: list[ClassInfo] | None = None) -> CallGraph:
|
|
162
|
+
"""Build a CallGraph from a single AbcFile.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
abc: The AbcFile to analyze.
|
|
166
|
+
classes: Optional class list for method name resolution.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Populated CallGraph.
|
|
170
|
+
"""
|
|
171
|
+
graph = cls()
|
|
172
|
+
method_name_map = _build_method_name_map(abc, classes or [])
|
|
173
|
+
|
|
174
|
+
for body in abc.method_bodies:
|
|
175
|
+
caller_name = method_name_map.get(
|
|
176
|
+
body.method, f"method_{body.method}")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
instructions = decode_instructions(body.code)
|
|
180
|
+
except Exception:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
for instr in instructions:
|
|
184
|
+
if instr.opcode in _MULTINAME_OPS and instr.operands:
|
|
185
|
+
mn_index = instr.operands[0]
|
|
186
|
+
target = resolve_multiname(abc, mn_index)
|
|
187
|
+
if target == "*" or target.startswith("multiname["):
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
edge = CallEdge(
|
|
191
|
+
caller=caller_name,
|
|
192
|
+
caller_method_index=body.method,
|
|
193
|
+
target=target,
|
|
194
|
+
opcode=instr.opcode,
|
|
195
|
+
mnemonic=instr.mnemonic,
|
|
196
|
+
offset=instr.offset,
|
|
197
|
+
edge_type=_classify_op(instr.opcode),
|
|
198
|
+
)
|
|
199
|
+
graph.edges.append(edge)
|
|
200
|
+
graph.callers_index[target].append(edge)
|
|
201
|
+
graph.callees_index[caller_name].append(edge)
|
|
202
|
+
|
|
203
|
+
return graph
|
|
204
|
+
|
|
205
|
+
def get_callers(self, target: str) -> list[CallEdge]:
|
|
206
|
+
"""Get all edges where *target* is called or referenced.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
target: Target method/property name.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of CallEdge objects referencing this target.
|
|
213
|
+
"""
|
|
214
|
+
return self.callers_index.get(target, [])
|
|
215
|
+
|
|
216
|
+
def get_callees(self, caller: str) -> list[CallEdge]:
|
|
217
|
+
"""Get all edges originating from *caller*.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
caller: Caller method name (``"Class.method"`` format).
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of CallEdge objects from this caller.
|
|
224
|
+
"""
|
|
225
|
+
return self.callees_index.get(caller, [])
|
|
226
|
+
|
|
227
|
+
def get_callers_by_type(self, target: str,
|
|
228
|
+
edge_type: str) -> list[CallEdge]:
|
|
229
|
+
"""Get edges of a specific type referencing *target*.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
target: Target name.
|
|
233
|
+
edge_type: One of ``"call"``, ``"construct"``, ``"read"``,
|
|
234
|
+
``"write"``, ``"class"``.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Filtered list of CallEdge objects.
|
|
238
|
+
"""
|
|
239
|
+
return [e for e in self.get_callers(target)
|
|
240
|
+
if e.edge_type == edge_type]
|
|
241
|
+
|
|
242
|
+
def get_instantiators(self, class_name: str) -> list[str]:
|
|
243
|
+
"""Get unique caller names that construct instances of *class_name*.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
class_name: The class being instantiated.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Sorted list of unique caller names.
|
|
250
|
+
"""
|
|
251
|
+
edges = self.get_callers_by_type(class_name, "construct")
|
|
252
|
+
return sorted(set(e.caller for e in edges))
|
|
253
|
+
|
|
254
|
+
def get_unique_callers(self, target: str) -> list[str]:
|
|
255
|
+
"""Get unique caller names for a target (calls only).
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
target: Target method name.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Sorted list of unique caller names.
|
|
262
|
+
"""
|
|
263
|
+
edges = self.get_callers_by_type(target, "call")
|
|
264
|
+
return sorted(set(e.caller for e in edges))
|
|
265
|
+
|
|
266
|
+
def get_unique_callees(self, caller: str) -> list[str]:
|
|
267
|
+
"""Get unique target names called by a caller (calls only).
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
caller: Caller method name.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Sorted list of unique target names.
|
|
274
|
+
"""
|
|
275
|
+
edges = [e for e in self.get_callees(caller) if e.edge_type == "call"]
|
|
276
|
+
return sorted(set(e.target for e in edges))
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def edge_count(self) -> int:
|
|
280
|
+
return len(self.edges)
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def unique_targets(self) -> int:
|
|
284
|
+
return len(self.callers_index)
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def unique_callers(self) -> int:
|
|
288
|
+
return len(self.callees_index)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _build_method_name_map(abc: AbcFile,
|
|
292
|
+
classes: list[ClassInfo]) -> dict[int, str]:
|
|
293
|
+
"""Build a mapping from method_index → ``"ClassName.methodName"``.
|
|
294
|
+
|
|
295
|
+
Uses ClassInfo's resolved methods to build readable names.
|
|
296
|
+
Falls back to ``"method_N"`` for methods not attached to classes.
|
|
297
|
+
"""
|
|
298
|
+
name_map: dict[int, str] = {}
|
|
299
|
+
|
|
300
|
+
for ci in classes:
|
|
301
|
+
# Instance constructor
|
|
302
|
+
name_map[ci.constructor_index] = f"{ci.name}.<init>"
|
|
303
|
+
# Static initializer
|
|
304
|
+
name_map[ci.static_init_index] = f"{ci.name}.<cinit>"
|
|
305
|
+
|
|
306
|
+
for m in ci.methods:
|
|
307
|
+
prefix = ""
|
|
308
|
+
if m.is_getter:
|
|
309
|
+
prefix = "get "
|
|
310
|
+
elif m.is_setter:
|
|
311
|
+
prefix = "set "
|
|
312
|
+
name_map[m.method_index] = f"{ci.name}.{prefix}{m.name}"
|
|
313
|
+
|
|
314
|
+
for m in ci.static_methods:
|
|
315
|
+
name_map[m.method_index] = f"{ci.name}.static {m.name}"
|
|
316
|
+
|
|
317
|
+
return name_map
|