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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
String pool analysis and search.
|
|
3
|
+
|
|
4
|
+
Provides filtered views into the ABC string pool and tracks where each
|
|
5
|
+
string constant is used in method bodies via OP_pushstring instructions.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from flashkit.workspace import Workspace
|
|
10
|
+
from flashkit.analysis.strings import StringIndex
|
|
11
|
+
|
|
12
|
+
ws = Workspace()
|
|
13
|
+
ws.load_swf("application.swf")
|
|
14
|
+
idx = StringIndex.from_workspace(ws)
|
|
15
|
+
|
|
16
|
+
results = idx.search("config")
|
|
17
|
+
classes = idx.classes_using_string("http://")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
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 OP_pushstring, OP_debugfile
|
|
29
|
+
from ..info.member_info import resolve_multiname, build_method_body_map
|
|
30
|
+
from ..info.class_info import ClassInfo
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class StringUsage:
|
|
35
|
+
"""A single occurrence of a string constant in bytecode.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
string: The string value.
|
|
39
|
+
class_name: Qualified name of the owning class.
|
|
40
|
+
method_name: Method name where the string is pushed.
|
|
41
|
+
method_index: Index into AbcFile.methods.
|
|
42
|
+
offset: Bytecode offset of the OP_pushstring instruction.
|
|
43
|
+
opcode: The opcode (OP_pushstring or OP_debugfile).
|
|
44
|
+
"""
|
|
45
|
+
string: str
|
|
46
|
+
class_name: str
|
|
47
|
+
method_name: str
|
|
48
|
+
method_index: int
|
|
49
|
+
offset: int
|
|
50
|
+
opcode: int = OP_pushstring
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class StringIndex:
|
|
55
|
+
"""Index of string constant usage across all method bodies.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
usages: All string usage entries.
|
|
59
|
+
by_string: Map of string value → list of usages.
|
|
60
|
+
by_class: Map of class name → list of usages.
|
|
61
|
+
all_strings: Set of all unique string values found in code.
|
|
62
|
+
pool_strings: Set of all strings in all string pools (superset).
|
|
63
|
+
"""
|
|
64
|
+
usages: list[StringUsage] = field(default_factory=list)
|
|
65
|
+
by_string: dict[str, list[StringUsage]] = field(
|
|
66
|
+
default_factory=lambda: defaultdict(list))
|
|
67
|
+
by_class: dict[str, list[StringUsage]] = field(
|
|
68
|
+
default_factory=lambda: defaultdict(list))
|
|
69
|
+
all_strings: set[str] = field(default_factory=set)
|
|
70
|
+
pool_strings: set[str] = field(default_factory=set)
|
|
71
|
+
|
|
72
|
+
def _add(self, usage: StringUsage) -> None:
|
|
73
|
+
"""Add a string usage to all indexes."""
|
|
74
|
+
self.usages.append(usage)
|
|
75
|
+
self.by_string[usage.string].append(usage)
|
|
76
|
+
self.by_class[usage.class_name].append(usage)
|
|
77
|
+
self.all_strings.add(usage.string)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_workspace(cls, workspace: object) -> StringIndex:
|
|
81
|
+
"""Build a StringIndex from a Workspace.
|
|
82
|
+
|
|
83
|
+
Walks all method bodies, decodes instructions, and collects
|
|
84
|
+
OP_pushstring and OP_debugfile references.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
workspace: A Workspace instance.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Populated StringIndex.
|
|
91
|
+
"""
|
|
92
|
+
from ..workspace.workspace import Workspace
|
|
93
|
+
ws: Workspace = workspace # type: ignore[assignment]
|
|
94
|
+
|
|
95
|
+
index = cls()
|
|
96
|
+
|
|
97
|
+
# Collect all pool strings
|
|
98
|
+
for abc in ws.abc_blocks:
|
|
99
|
+
for s in abc.string_pool:
|
|
100
|
+
if s:
|
|
101
|
+
index.pool_strings.add(s)
|
|
102
|
+
|
|
103
|
+
for abc in ws.abc_blocks:
|
|
104
|
+
index._index_abc(abc, ws.classes)
|
|
105
|
+
|
|
106
|
+
return index
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_abc(cls, abc: AbcFile,
|
|
110
|
+
classes: list[ClassInfo] | None = None) -> StringIndex:
|
|
111
|
+
"""Build a StringIndex from a single AbcFile.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
abc: The AbcFile to analyze.
|
|
115
|
+
classes: Optional class list for method name resolution.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Populated StringIndex.
|
|
119
|
+
"""
|
|
120
|
+
index = cls()
|
|
121
|
+
for s in abc.string_pool:
|
|
122
|
+
if s:
|
|
123
|
+
index.pool_strings.add(s)
|
|
124
|
+
index._index_abc(abc, classes or [])
|
|
125
|
+
return index
|
|
126
|
+
|
|
127
|
+
def _index_abc(self, abc: AbcFile, classes: list[ClassInfo]) -> None:
|
|
128
|
+
"""Walk all method bodies in an AbcFile and index string usages."""
|
|
129
|
+
method_owner_map = _build_method_owner_map(abc, classes)
|
|
130
|
+
method_name_map = _build_method_name_map(abc, classes)
|
|
131
|
+
|
|
132
|
+
for body in abc.method_bodies:
|
|
133
|
+
owner_class = method_owner_map.get(body.method, "")
|
|
134
|
+
method_name = method_name_map.get(
|
|
135
|
+
body.method, f"method_{body.method}")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
instructions = decode_instructions(body.code)
|
|
139
|
+
except Exception:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
for instr in instructions:
|
|
143
|
+
if instr.opcode in (OP_pushstring, OP_debugfile):
|
|
144
|
+
if not instr.operands:
|
|
145
|
+
continue
|
|
146
|
+
str_index = instr.operands[0]
|
|
147
|
+
if 0 < str_index < len(abc.string_pool):
|
|
148
|
+
string_val = abc.string_pool[str_index]
|
|
149
|
+
self._add(StringUsage(
|
|
150
|
+
string=string_val,
|
|
151
|
+
class_name=owner_class,
|
|
152
|
+
method_name=method_name,
|
|
153
|
+
method_index=body.method,
|
|
154
|
+
offset=instr.offset,
|
|
155
|
+
opcode=instr.opcode,
|
|
156
|
+
))
|
|
157
|
+
|
|
158
|
+
def search(self, pattern: str, regex: bool = False) -> list[str]:
|
|
159
|
+
"""Search for strings matching a pattern.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
pattern: Substring to search for, or regex if regex=True.
|
|
163
|
+
regex: If True, treat pattern as a regular expression.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of matching string values (from code usage).
|
|
167
|
+
"""
|
|
168
|
+
if regex:
|
|
169
|
+
try:
|
|
170
|
+
compiled = re.compile(pattern, re.IGNORECASE)
|
|
171
|
+
except re.error:
|
|
172
|
+
return []
|
|
173
|
+
return sorted(s for s in self.all_strings if compiled.search(s))
|
|
174
|
+
else:
|
|
175
|
+
pattern_lower = pattern.lower()
|
|
176
|
+
return sorted(
|
|
177
|
+
s for s in self.all_strings if pattern_lower in s.lower())
|
|
178
|
+
|
|
179
|
+
def search_pool(self, pattern: str, regex: bool = False) -> list[str]:
|
|
180
|
+
"""Search all string pool entries (not just those used in code).
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
pattern: Substring or regex pattern.
|
|
184
|
+
regex: If True, treat as regex.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of matching strings from the pool.
|
|
188
|
+
"""
|
|
189
|
+
if regex:
|
|
190
|
+
try:
|
|
191
|
+
compiled = re.compile(pattern, re.IGNORECASE)
|
|
192
|
+
except re.error:
|
|
193
|
+
return []
|
|
194
|
+
return sorted(s for s in self.pool_strings if compiled.search(s))
|
|
195
|
+
else:
|
|
196
|
+
pattern_lower = pattern.lower()
|
|
197
|
+
return sorted(
|
|
198
|
+
s for s in self.pool_strings if pattern_lower in s.lower())
|
|
199
|
+
|
|
200
|
+
def strings_in_class(self, class_name: str) -> list[str]:
|
|
201
|
+
"""Get all unique strings referenced by a class.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
class_name: Qualified or simple class name.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Sorted list of unique string values.
|
|
208
|
+
"""
|
|
209
|
+
usages = self.by_class.get(class_name, [])
|
|
210
|
+
if not usages:
|
|
211
|
+
# Try simple name match
|
|
212
|
+
for key, u_list in self.by_class.items():
|
|
213
|
+
if key.endswith(f".{class_name}") or key == class_name:
|
|
214
|
+
usages = u_list
|
|
215
|
+
break
|
|
216
|
+
return sorted(set(u.string for u in usages))
|
|
217
|
+
|
|
218
|
+
def classes_using_string(self, string: str) -> list[str]:
|
|
219
|
+
"""Get all classes that reference a specific string.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
string: The exact string value.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Sorted list of unique class qualified names.
|
|
226
|
+
"""
|
|
227
|
+
return sorted(set(
|
|
228
|
+
u.class_name for u in self.by_string.get(string, [])
|
|
229
|
+
if u.class_name
|
|
230
|
+
))
|
|
231
|
+
|
|
232
|
+
def debug_markers(self) -> list[str]:
|
|
233
|
+
"""Find strings that look like debug source markers (e.g. ``[Foo.hx]``).
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Sorted list of matching strings.
|
|
237
|
+
"""
|
|
238
|
+
return sorted(
|
|
239
|
+
s for s in self.all_strings
|
|
240
|
+
if s.endswith(".hx") or s.endswith(".as")
|
|
241
|
+
or (s.startswith("[") and s.endswith("]"))
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def url_strings(self) -> list[str]:
|
|
245
|
+
"""Find strings that look like URLs or file paths.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Sorted list of matching strings.
|
|
249
|
+
"""
|
|
250
|
+
return sorted(
|
|
251
|
+
s for s in self.all_strings
|
|
252
|
+
if s.startswith("http://") or s.startswith("https://")
|
|
253
|
+
or s.startswith("file://") or s.startswith("/")
|
|
254
|
+
or ".xml" in s or ".json" in s or ".swf" in s
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def ui_strings(self) -> list[str]:
|
|
258
|
+
"""Find strings that likely represent UI labels (contain spaces, mixed case).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Sorted list of matching strings.
|
|
262
|
+
"""
|
|
263
|
+
return sorted(
|
|
264
|
+
s for s in self.all_strings
|
|
265
|
+
if " " in s and len(s) > 3 and not s.startswith("http")
|
|
266
|
+
and not s.endswith(".hx") and not s.endswith(".as")
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def unique_string_count(self) -> int:
|
|
271
|
+
return len(self.all_strings)
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def total_usages(self) -> int:
|
|
275
|
+
return len(self.usages)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _build_method_owner_map(abc: AbcFile,
|
|
279
|
+
classes: list[ClassInfo]) -> dict[int, str]:
|
|
280
|
+
"""Map method_index → owning class qualified name."""
|
|
281
|
+
owner: dict[int, str] = {}
|
|
282
|
+
for ci in classes:
|
|
283
|
+
owner[ci.constructor_index] = ci.qualified_name
|
|
284
|
+
owner[ci.static_init_index] = ci.qualified_name
|
|
285
|
+
for m in ci.all_methods:
|
|
286
|
+
owner[m.method_index] = ci.qualified_name
|
|
287
|
+
return owner
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _build_method_name_map(abc: AbcFile,
|
|
291
|
+
classes: list[ClassInfo]) -> dict[int, str]:
|
|
292
|
+
"""Map method_index → readable method name."""
|
|
293
|
+
name_map: dict[int, str] = {}
|
|
294
|
+
for ci in classes:
|
|
295
|
+
name_map[ci.constructor_index] = "<init>"
|
|
296
|
+
name_map[ci.static_init_index] = "<cinit>"
|
|
297
|
+
for m in ci.all_methods:
|
|
298
|
+
name_map[m.method_index] = m.name
|
|
299
|
+
return name_map
|
flashkit/cli/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flashkit CLI — command-line interface for SWF/ABC analysis.
|
|
3
|
+
|
|
4
|
+
Structured as a package with one module per subcommand.
|
|
5
|
+
Entry point is :func:`main`, registered as ``flashkit`` console script.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .. import __version__
|
|
14
|
+
from ..errors import FlashkitError
|
|
15
|
+
from ._util import red
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
19
|
+
"""Build the top-level argument parser with all subcommands."""
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="flashkit",
|
|
22
|
+
description="SWF/ABC toolkit — inspect, analyze, and manipulate Flash files.",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--version", action="version",
|
|
26
|
+
version=f"flashkit {__version__}")
|
|
27
|
+
|
|
28
|
+
sub = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
29
|
+
|
|
30
|
+
# Import each command module — each one registers itself.
|
|
31
|
+
from . import (
|
|
32
|
+
info, tags, classes, class_cmd, strings,
|
|
33
|
+
disasm, callers, callees, refs, tree,
|
|
34
|
+
packages, extract, build,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
info.register(sub)
|
|
38
|
+
tags.register(sub)
|
|
39
|
+
classes.register(sub)
|
|
40
|
+
class_cmd.register(sub)
|
|
41
|
+
strings.register(sub)
|
|
42
|
+
disasm.register(sub)
|
|
43
|
+
callers.register(sub)
|
|
44
|
+
callees.register(sub)
|
|
45
|
+
refs.register(sub)
|
|
46
|
+
tree.register(sub)
|
|
47
|
+
packages.register(sub)
|
|
48
|
+
extract.register(sub)
|
|
49
|
+
build.register(sub)
|
|
50
|
+
|
|
51
|
+
return parser
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: list[str] | None = None) -> None:
|
|
55
|
+
"""CLI entry point."""
|
|
56
|
+
parser = build_parser()
|
|
57
|
+
args = parser.parse_args(argv)
|
|
58
|
+
|
|
59
|
+
if not args.command:
|
|
60
|
+
parser.print_help()
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
args.func(args)
|
|
65
|
+
except FlashkitError as e:
|
|
66
|
+
print(f"{red('Error')}: {e}", file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
except BrokenPipeError:
|
|
69
|
+
pass
|
|
70
|
+
except KeyboardInterrupt:
|
|
71
|
+
sys.exit(130)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
flashkit/cli/_util.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Shared CLI utilities — ANSI colors and workspace loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from ..workspace import Workspace
|
|
9
|
+
|
|
10
|
+
# Respect NO_COLOR convention (https://no-color.org/) and non-TTY output.
|
|
11
|
+
_NO_COLOR = not sys.stdout.isatty() or os.environ.get("NO_COLOR", "") != ""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _c(code: str, text: str) -> str:
|
|
15
|
+
if _NO_COLOR:
|
|
16
|
+
return text
|
|
17
|
+
return f"\033[{code}m{text}\033[0m"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bold(t: str) -> str:
|
|
21
|
+
return _c("1", t)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def dim(t: str) -> str:
|
|
25
|
+
return _c("2", t)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def green(t: str) -> str:
|
|
29
|
+
return _c("32", t)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cyan(t: str) -> str:
|
|
33
|
+
return _c("36", t)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def yellow(t: str) -> str:
|
|
37
|
+
return _c("33", t)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def red(t: str) -> str:
|
|
41
|
+
return _c("31", t)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def magenta(t: str) -> str:
|
|
45
|
+
return _c("35", t)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load(path: str) -> Workspace:
|
|
49
|
+
"""Load a SWF/SWZ file into a Workspace."""
|
|
50
|
+
ws = Workspace()
|
|
51
|
+
ws.load(path)
|
|
52
|
+
return ws
|
flashkit/cli/build.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""``flashkit build`` — rebuild a SWF (recompress or decompress)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ._util import load
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
12
|
+
p = sub.add_parser("build", help="Rebuild SWF (recompress/decompress)")
|
|
13
|
+
p.add_argument("file", help="SWF file")
|
|
14
|
+
p.add_argument("-o", "--output", help="Output file path")
|
|
15
|
+
p.add_argument("-d", "--decompress", action="store_true",
|
|
16
|
+
help="Output uncompressed FWS")
|
|
17
|
+
p.set_defaults(func=run)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(args: argparse.Namespace) -> None:
|
|
21
|
+
ws = load(args.file)
|
|
22
|
+
res = ws.resources[0]
|
|
23
|
+
|
|
24
|
+
if res.swf_tags is None:
|
|
25
|
+
print("Cannot rebuild: not a SWF file.")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
from ..swf.builder import rebuild_swf
|
|
29
|
+
|
|
30
|
+
compress = not args.decompress
|
|
31
|
+
output = rebuild_swf(res.swf_header, res.swf_tags, compress=compress)
|
|
32
|
+
|
|
33
|
+
out_path = args.output or args.file
|
|
34
|
+
Path(out_path).write_bytes(output)
|
|
35
|
+
mode = "compressed" if compress else "uncompressed"
|
|
36
|
+
print(f"Wrote {out_path} ({len(output)} bytes, {mode})")
|
flashkit/cli/callees.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""``flashkit callees`` — find calls made from a method."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ._util import load, bold, dim
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
11
|
+
p = sub.add_parser("callees", help="Find calls from a method")
|
|
12
|
+
p.add_argument("file", help="SWF or SWZ file")
|
|
13
|
+
p.add_argument("name", help="Method name (e.g. Class.method)")
|
|
14
|
+
p.set_defaults(func=run)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(args: argparse.Namespace) -> None:
|
|
18
|
+
ws = load(args.file)
|
|
19
|
+
|
|
20
|
+
from ..analysis.call_graph import CallGraph
|
|
21
|
+
graph = CallGraph.from_workspace(ws)
|
|
22
|
+
edges = graph.get_callees(args.name)
|
|
23
|
+
|
|
24
|
+
if not edges:
|
|
25
|
+
print(f"No callees found for '{args.name}'.")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
print(bold(f"Callees from '{args.name}'") + f" ({len(edges)} edges)")
|
|
29
|
+
for e in edges:
|
|
30
|
+
print(f" -> {e.target} {dim(e.mnemonic)} @ 0x{e.offset:04X}")
|
flashkit/cli/callers.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""``flashkit callers`` — find callers of a method/property."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ._util import load, bold, dim
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
11
|
+
p = sub.add_parser("callers", help="Find callers of a method")
|
|
12
|
+
p.add_argument("file", help="SWF or SWZ file")
|
|
13
|
+
p.add_argument("name", help="Method or property name")
|
|
14
|
+
p.set_defaults(func=run)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(args: argparse.Namespace) -> None:
|
|
18
|
+
ws = load(args.file)
|
|
19
|
+
|
|
20
|
+
from ..analysis.call_graph import CallGraph
|
|
21
|
+
graph = CallGraph.from_workspace(ws)
|
|
22
|
+
edges = graph.get_callers(args.name)
|
|
23
|
+
|
|
24
|
+
if not edges:
|
|
25
|
+
print(f"No callers found for '{args.name}'.")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
print(bold(f"Callers of '{args.name}'") + f" ({len(edges)} edges)")
|
|
29
|
+
for e in edges:
|
|
30
|
+
print(f" {e.caller} {dim(e.mnemonic)} @ 0x{e.offset:04X}")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""``flashkit class`` — show details for a single class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ._util import load, bold, dim, cyan, green, yellow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
11
|
+
p = sub.add_parser("class", help="Show class details")
|
|
12
|
+
p.add_argument("file", help="SWF or SWZ file")
|
|
13
|
+
p.add_argument("name", help="Class name (simple or qualified)")
|
|
14
|
+
p.set_defaults(func=run)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(args: argparse.Namespace) -> None:
|
|
18
|
+
ws = load(args.file)
|
|
19
|
+
cls = ws.get_class(args.name)
|
|
20
|
+
|
|
21
|
+
if cls is None:
|
|
22
|
+
matches = ws.find_classes(name=args.name)
|
|
23
|
+
if len(matches) == 1:
|
|
24
|
+
cls = matches[0]
|
|
25
|
+
elif matches:
|
|
26
|
+
print(f"Ambiguous name '{args.name}', matches:")
|
|
27
|
+
for m in matches:
|
|
28
|
+
print(f" {m.qualified_name}")
|
|
29
|
+
return
|
|
30
|
+
else:
|
|
31
|
+
print(f"Class '{args.name}' not found.")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
flags = []
|
|
35
|
+
if cls.is_interface:
|
|
36
|
+
flags.append("interface")
|
|
37
|
+
if cls.is_final:
|
|
38
|
+
flags.append("final")
|
|
39
|
+
if cls.is_sealed:
|
|
40
|
+
flags.append("sealed")
|
|
41
|
+
flag_str = f" [{', '.join(flags)}]" if flags else ""
|
|
42
|
+
|
|
43
|
+
print(bold(cls.qualified_name) + dim(flag_str))
|
|
44
|
+
if cls.package:
|
|
45
|
+
print(f" Package: {cyan(cls.package)}")
|
|
46
|
+
print(f" Extends: {cyan(cls.super_name)}")
|
|
47
|
+
if cls.interfaces:
|
|
48
|
+
print(f" Implements: {', '.join(green(i) for i in cls.interfaces)}")
|
|
49
|
+
|
|
50
|
+
if cls.fields:
|
|
51
|
+
print(f"\n {bold('Instance Fields')} ({len(cls.fields)})")
|
|
52
|
+
for f in cls.fields:
|
|
53
|
+
const = "const " if f.is_const else ""
|
|
54
|
+
print(f" {const}{f.name}: {yellow(f.type_name)}")
|
|
55
|
+
|
|
56
|
+
if cls.static_fields:
|
|
57
|
+
print(f"\n {bold('Static Fields')} ({len(cls.static_fields)})")
|
|
58
|
+
for f in cls.static_fields:
|
|
59
|
+
const = "const " if f.is_const else ""
|
|
60
|
+
print(f" static {const}{f.name}: {yellow(f.type_name)}")
|
|
61
|
+
|
|
62
|
+
if cls.methods:
|
|
63
|
+
print(f"\n {bold('Instance Methods')} ({len(cls.methods)})")
|
|
64
|
+
for m in cls.methods:
|
|
65
|
+
kind = ""
|
|
66
|
+
if m.is_getter:
|
|
67
|
+
kind = "get "
|
|
68
|
+
elif m.is_setter:
|
|
69
|
+
kind = "set "
|
|
70
|
+
params = ", ".join(
|
|
71
|
+
f"{n}: {t}" if n else t
|
|
72
|
+
for n, t in zip(m.param_names or [""] * len(m.param_types),
|
|
73
|
+
m.param_types))
|
|
74
|
+
print(f" {kind}{m.name}({params}): {yellow(m.return_type)}")
|
|
75
|
+
|
|
76
|
+
if cls.static_methods:
|
|
77
|
+
print(f"\n {bold('Static Methods')} ({len(cls.static_methods)})")
|
|
78
|
+
for m in cls.static_methods:
|
|
79
|
+
params = ", ".join(
|
|
80
|
+
f"{n}: {t}" if n else t
|
|
81
|
+
for n, t in zip(m.param_names or [""] * len(m.param_types),
|
|
82
|
+
m.param_types))
|
|
83
|
+
print(f" static {m.name}({params}): {yellow(m.return_type)}")
|
flashkit/cli/classes.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""``flashkit classes`` — list all classes with optional filters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ._util import load, bold, dim, cyan, green
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
11
|
+
p = sub.add_parser("classes", help="List classes")
|
|
12
|
+
p.add_argument("file", help="SWF or SWZ file")
|
|
13
|
+
p.add_argument("-s", "--search", help="Filter by name substring")
|
|
14
|
+
p.add_argument("-p", "--package", help="Filter by package")
|
|
15
|
+
p.add_argument("-e", "--extends", help="Filter by superclass")
|
|
16
|
+
p.add_argument("-i", "--interfaces-only", action="store_true",
|
|
17
|
+
help="Show only interfaces")
|
|
18
|
+
p.add_argument("-v", "--verbose", action="store_true",
|
|
19
|
+
help="Show detailed info per class")
|
|
20
|
+
p.set_defaults(func=run)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(args: argparse.Namespace) -> None:
|
|
24
|
+
ws = load(args.file)
|
|
25
|
+
classes = ws.classes
|
|
26
|
+
|
|
27
|
+
if args.package:
|
|
28
|
+
classes = [c for c in classes if c.package == args.package]
|
|
29
|
+
if args.extends:
|
|
30
|
+
classes = [c for c in classes if c.super_name == args.extends]
|
|
31
|
+
if args.interfaces_only:
|
|
32
|
+
classes = [c for c in classes if c.is_interface]
|
|
33
|
+
if args.search:
|
|
34
|
+
term = args.search.lower()
|
|
35
|
+
classes = [c for c in classes if term in c.qualified_name.lower()]
|
|
36
|
+
|
|
37
|
+
if not classes:
|
|
38
|
+
print("No classes found.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if args.verbose:
|
|
42
|
+
for cls in classes:
|
|
43
|
+
flags = []
|
|
44
|
+
if cls.is_interface:
|
|
45
|
+
flags.append("interface")
|
|
46
|
+
if cls.is_final:
|
|
47
|
+
flags.append("final")
|
|
48
|
+
if cls.is_sealed:
|
|
49
|
+
flags.append("sealed")
|
|
50
|
+
flag_str = f" [{', '.join(flags)}]" if flags else ""
|
|
51
|
+
|
|
52
|
+
print(bold(cls.qualified_name) + dim(flag_str))
|
|
53
|
+
print(f" extends {cyan(cls.super_name)}")
|
|
54
|
+
if cls.interfaces:
|
|
55
|
+
print(f" implements {', '.join(green(i) for i in cls.interfaces)}")
|
|
56
|
+
print(f" {len(cls.fields)} fields, {len(cls.methods)} methods"
|
|
57
|
+
f", {len(cls.static_fields)} static fields"
|
|
58
|
+
f", {len(cls.static_methods)} static methods")
|
|
59
|
+
print()
|
|
60
|
+
else:
|
|
61
|
+
print(bold(f"{'Class':<50} {'Super':<25} {'Fields':>6} {'Methods':>7}"))
|
|
62
|
+
print("-" * 92)
|
|
63
|
+
for cls in classes:
|
|
64
|
+
name = cls.qualified_name
|
|
65
|
+
prefix = ""
|
|
66
|
+
if cls.is_interface:
|
|
67
|
+
prefix = dim("[I] ")
|
|
68
|
+
print(f"{prefix}{name:<50} {cls.super_name:<25} "
|
|
69
|
+
f"{len(cls.all_fields):>6} {len(cls.all_methods):>7}")
|
|
70
|
+
|
|
71
|
+
print(f"\n{len(classes)} class(es)")
|