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,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource: a single loaded SWF or SWZ file.
|
|
3
|
+
|
|
4
|
+
A Resource holds the parsed content of one file — its SWF tags (if SWF),
|
|
5
|
+
the extracted AbcFile objects, and the resolved ClassInfo list.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..errors import ResourceError
|
|
14
|
+
from ..abc.types import AbcFile
|
|
15
|
+
from ..abc.parser import parse_abc
|
|
16
|
+
from ..abc.writer import serialize_abc
|
|
17
|
+
from ..swf.tags import SWFTag, TAG_DO_ABC, TAG_DO_ABC2
|
|
18
|
+
from ..swf.parser import parse_swf
|
|
19
|
+
from ..info.class_info import ClassInfo, build_all_classes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Resource:
|
|
24
|
+
"""A loaded SWF or SWZ file with parsed ABC content.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
path: Original file path.
|
|
28
|
+
kind: File type (``"swf"`` or ``"swz"``).
|
|
29
|
+
swf_header: SWF header bytes (None for SWZ).
|
|
30
|
+
swf_tags: SWF tag list (None for SWZ).
|
|
31
|
+
swf_version: SWF version number (None for SWZ).
|
|
32
|
+
abc_blocks: List of parsed AbcFile objects from this resource.
|
|
33
|
+
classes: All ClassInfo objects resolved from the ABC blocks.
|
|
34
|
+
"""
|
|
35
|
+
path: str = ""
|
|
36
|
+
kind: str = "swf"
|
|
37
|
+
swf_header: bytes | None = None
|
|
38
|
+
swf_tags: list[SWFTag] | None = None
|
|
39
|
+
swf_version: int | None = None
|
|
40
|
+
abc_blocks: list[AbcFile] = field(default_factory=list)
|
|
41
|
+
classes: list[ClassInfo] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def class_count(self) -> int:
|
|
45
|
+
return len(self.classes)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def method_count(self) -> int:
|
|
49
|
+
return sum(len(abc.methods) for abc in self.abc_blocks)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def string_count(self) -> int:
|
|
53
|
+
return sum(len(abc.string_pool) for abc in self.abc_blocks)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_abc_from_tag(tag: SWFTag) -> bytes | None:
|
|
57
|
+
"""Extract raw ABC bytes from a DoABC or DoABC2 tag."""
|
|
58
|
+
if tag.tag_type == TAG_DO_ABC:
|
|
59
|
+
return tag.payload
|
|
60
|
+
elif tag.tag_type == TAG_DO_ABC2 and len(tag.payload) > 4:
|
|
61
|
+
try:
|
|
62
|
+
null_idx = tag.payload.index(0, 4)
|
|
63
|
+
return tag.payload[null_idx + 1:]
|
|
64
|
+
except ValueError:
|
|
65
|
+
return None
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_swf(path: str | Path) -> Resource:
|
|
70
|
+
"""Load a SWF file into a Resource.
|
|
71
|
+
|
|
72
|
+
Parses the SWF, extracts all DoABC/DoABC2 tags, parses each into
|
|
73
|
+
an AbcFile, and resolves all classes.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
path: Path to the SWF file.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Resource with all ABC content and resolved classes.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ResourceError: If the file cannot be read or is not a valid SWF.
|
|
83
|
+
"""
|
|
84
|
+
path = Path(path)
|
|
85
|
+
try:
|
|
86
|
+
with open(path, "rb") as f:
|
|
87
|
+
data = f.read()
|
|
88
|
+
except OSError as e:
|
|
89
|
+
raise ResourceError(f"Cannot read SWF file '{path}': {e}") from e
|
|
90
|
+
|
|
91
|
+
if not data:
|
|
92
|
+
raise ResourceError(f"SWF file is empty: '{path}'")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
header, tags, version, file_length = parse_swf(data)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise ResourceError(f"Failed to parse SWF '{path}': {e}") from e
|
|
98
|
+
|
|
99
|
+
abc_blocks: list[AbcFile] = []
|
|
100
|
+
all_classes: list[ClassInfo] = []
|
|
101
|
+
|
|
102
|
+
for tag in tags:
|
|
103
|
+
abc_data = _extract_abc_from_tag(tag)
|
|
104
|
+
if abc_data and len(abc_data) > 4:
|
|
105
|
+
abc = parse_abc(abc_data)
|
|
106
|
+
abc_blocks.append(abc)
|
|
107
|
+
all_classes.extend(build_all_classes(abc))
|
|
108
|
+
|
|
109
|
+
return Resource(
|
|
110
|
+
path=str(path),
|
|
111
|
+
kind="swf",
|
|
112
|
+
swf_header=header,
|
|
113
|
+
swf_tags=tags,
|
|
114
|
+
swf_version=version,
|
|
115
|
+
abc_blocks=abc_blocks,
|
|
116
|
+
classes=all_classes,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_swz(path: str | Path) -> Resource:
|
|
121
|
+
"""Load a SWZ file into a Resource.
|
|
122
|
+
|
|
123
|
+
SWZ files are signed and compressed ABC modules used by Adobe AIR.
|
|
124
|
+
Format: RSA signature (variable length) + zlib-compressed ABC data.
|
|
125
|
+
There is no fixed magic header — we scan for a valid zlib stream
|
|
126
|
+
and verify the decompressed content starts with ABC version 46.16.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Path to the SWZ file.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Resource with the parsed ABC content and resolved classes.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ResourceError: If the file cannot be read or contains no valid ABC.
|
|
136
|
+
"""
|
|
137
|
+
import zlib
|
|
138
|
+
|
|
139
|
+
path = Path(path)
|
|
140
|
+
try:
|
|
141
|
+
with open(path, "rb") as f:
|
|
142
|
+
data = f.read()
|
|
143
|
+
except OSError as e:
|
|
144
|
+
raise ResourceError(f"Cannot read SWZ file '{path}': {e}") from e
|
|
145
|
+
|
|
146
|
+
if not data:
|
|
147
|
+
raise ResourceError(f"SWZ file is empty: '{path}'")
|
|
148
|
+
|
|
149
|
+
# SWZ format: RSA signature + zlib-compressed ABC.
|
|
150
|
+
# Scan for a zlib stream (0x78 byte) and verify decompressed ABC version.
|
|
151
|
+
abc_data = None
|
|
152
|
+
for i in range(min(len(data), 256)):
|
|
153
|
+
if data[i] == 0x78 and i + 2 < len(data):
|
|
154
|
+
try:
|
|
155
|
+
decompressed = zlib.decompress(data[i:])
|
|
156
|
+
if len(decompressed) >= 4:
|
|
157
|
+
minor = decompressed[0] | (decompressed[1] << 8)
|
|
158
|
+
major = decompressed[2] | (decompressed[3] << 8)
|
|
159
|
+
if major == 46 and minor == 16:
|
|
160
|
+
abc_data = decompressed
|
|
161
|
+
break
|
|
162
|
+
except zlib.error:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if abc_data is None:
|
|
166
|
+
# Try raw (uncompressed) ABC
|
|
167
|
+
if len(data) >= 4:
|
|
168
|
+
minor = data[0] | (data[1] << 8)
|
|
169
|
+
major = data[2] | (data[3] << 8)
|
|
170
|
+
if major == 46 and minor == 16:
|
|
171
|
+
abc_data = data
|
|
172
|
+
|
|
173
|
+
if abc_data is None:
|
|
174
|
+
raise ResourceError(
|
|
175
|
+
f"No valid ABC data found in SWZ file: '{path}'")
|
|
176
|
+
|
|
177
|
+
abc_blocks: list[AbcFile] = []
|
|
178
|
+
all_classes: list[ClassInfo] = []
|
|
179
|
+
|
|
180
|
+
abc = parse_abc(abc_data)
|
|
181
|
+
abc_blocks.append(abc)
|
|
182
|
+
all_classes.extend(build_all_classes(abc))
|
|
183
|
+
|
|
184
|
+
return Resource(
|
|
185
|
+
path=str(path),
|
|
186
|
+
kind="swz",
|
|
187
|
+
abc_blocks=abc_blocks,
|
|
188
|
+
classes=all_classes,
|
|
189
|
+
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workspace: the top-level container for loaded SWF/SWZ content.
|
|
3
|
+
|
|
4
|
+
The Workspace loads one or more files, aggregates all ABC content,
|
|
5
|
+
and provides unified access to classes, strings, and analysis.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from flashkit.workspace import Workspace
|
|
10
|
+
|
|
11
|
+
ws = Workspace()
|
|
12
|
+
ws.load_swf("application.swf")
|
|
13
|
+
ws.load_swz("module.swz")
|
|
14
|
+
|
|
15
|
+
for cls in ws.classes:
|
|
16
|
+
print(f"{cls.qualified_name} ({len(cls.fields)} fields)")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ..abc.types import AbcFile
|
|
24
|
+
from ..info.class_info import ClassInfo
|
|
25
|
+
from ..info.package_info import PackageInfo, group_by_package
|
|
26
|
+
from .resource import Resource, load_swf, load_swz
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Workspace:
|
|
30
|
+
"""Unified workspace for analyzing SWF/SWZ content.
|
|
31
|
+
|
|
32
|
+
Load one or more files, then query the aggregated class index.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
resources: List of loaded Resource objects.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self.resources: list[Resource] = []
|
|
40
|
+
self._class_index: dict[str, ClassInfo] = {}
|
|
41
|
+
self._classes: list[ClassInfo] = []
|
|
42
|
+
self._packages: list[PackageInfo] | None = None
|
|
43
|
+
|
|
44
|
+
def load_swf(self, path: str | Path) -> Resource:
|
|
45
|
+
"""Load a SWF file into the workspace.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
path: Path to the SWF file.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The loaded Resource.
|
|
52
|
+
"""
|
|
53
|
+
res = load_swf(path)
|
|
54
|
+
self._add_resource(res)
|
|
55
|
+
return res
|
|
56
|
+
|
|
57
|
+
def load_swz(self, path: str | Path) -> Resource:
|
|
58
|
+
"""Load a SWZ file into the workspace.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
path: Path to the SWZ file.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The loaded Resource.
|
|
65
|
+
"""
|
|
66
|
+
res = load_swz(path)
|
|
67
|
+
self._add_resource(res)
|
|
68
|
+
return res
|
|
69
|
+
|
|
70
|
+
def load_swf_bytes(self, data: bytes, name: str = "<memory>") -> Resource:
|
|
71
|
+
"""Load a SWF from raw bytes (no file needed).
|
|
72
|
+
|
|
73
|
+
Useful for programmatically constructed SWFs or testing.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data: Raw SWF file bytes.
|
|
77
|
+
name: Display name for the resource.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The loaded Resource.
|
|
81
|
+
"""
|
|
82
|
+
from ..swf.parser import parse_swf
|
|
83
|
+
from ..swf.tags import TAG_DO_ABC, TAG_DO_ABC2
|
|
84
|
+
from ..abc.parser import parse_abc
|
|
85
|
+
from ..info.class_info import build_all_classes
|
|
86
|
+
|
|
87
|
+
header, tags, version, file_length = parse_swf(data)
|
|
88
|
+
abc_blocks: list[AbcFile] = []
|
|
89
|
+
all_classes: list[ClassInfo] = []
|
|
90
|
+
|
|
91
|
+
for tag in tags:
|
|
92
|
+
abc_data = None
|
|
93
|
+
if tag.tag_type == TAG_DO_ABC:
|
|
94
|
+
abc_data = tag.payload
|
|
95
|
+
elif tag.tag_type == TAG_DO_ABC2 and len(tag.payload) > 4:
|
|
96
|
+
try:
|
|
97
|
+
null_idx = tag.payload.index(0, 4)
|
|
98
|
+
abc_data = tag.payload[null_idx + 1:]
|
|
99
|
+
except ValueError:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
if abc_data and len(abc_data) > 4:
|
|
103
|
+
abc = parse_abc(abc_data)
|
|
104
|
+
abc_blocks.append(abc)
|
|
105
|
+
all_classes.extend(build_all_classes(abc))
|
|
106
|
+
|
|
107
|
+
res = Resource(
|
|
108
|
+
path=name,
|
|
109
|
+
kind="swf",
|
|
110
|
+
swf_header=header,
|
|
111
|
+
swf_tags=tags,
|
|
112
|
+
swf_version=version,
|
|
113
|
+
abc_blocks=abc_blocks,
|
|
114
|
+
classes=all_classes,
|
|
115
|
+
)
|
|
116
|
+
self._add_resource(res)
|
|
117
|
+
return res
|
|
118
|
+
|
|
119
|
+
def load(self, path: str | Path) -> Resource:
|
|
120
|
+
"""Load a file, auto-detecting format by extension.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
path: Path to a SWF or SWZ file.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The loaded Resource.
|
|
127
|
+
"""
|
|
128
|
+
p = Path(path)
|
|
129
|
+
if p.suffix.lower() == ".swz":
|
|
130
|
+
return self.load_swz(p)
|
|
131
|
+
else:
|
|
132
|
+
return self.load_swf(p)
|
|
133
|
+
|
|
134
|
+
def _add_resource(self, res: Resource) -> None:
|
|
135
|
+
"""Add a resource and update indexes."""
|
|
136
|
+
self.resources.append(res)
|
|
137
|
+
for cls in res.classes:
|
|
138
|
+
self._classes.append(cls)
|
|
139
|
+
# Index by both simple name and qualified name
|
|
140
|
+
self._class_index[cls.name] = cls
|
|
141
|
+
if cls.qualified_name != cls.name:
|
|
142
|
+
self._class_index[cls.qualified_name] = cls
|
|
143
|
+
self._packages = None # invalidate cache
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def classes(self) -> list[ClassInfo]:
|
|
147
|
+
"""All classes across all loaded resources."""
|
|
148
|
+
return self._classes
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def abc_blocks(self) -> list[AbcFile]:
|
|
152
|
+
"""All AbcFile objects across all loaded resources."""
|
|
153
|
+
result: list[AbcFile] = []
|
|
154
|
+
for res in self.resources:
|
|
155
|
+
result.extend(res.abc_blocks)
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def packages(self) -> list[PackageInfo]:
|
|
160
|
+
"""All packages, computed from the class index."""
|
|
161
|
+
if self._packages is None:
|
|
162
|
+
self._packages = group_by_package(self._classes)
|
|
163
|
+
return self._packages
|
|
164
|
+
|
|
165
|
+
def get_class(self, name: str) -> ClassInfo | None:
|
|
166
|
+
"""Look up a class by name or qualified name.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
name: Simple name (e.g. ``"MyClass"``) or qualified
|
|
170
|
+
(e.g. ``"com.example.MyClass"``).
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
ClassInfo if found, None otherwise.
|
|
174
|
+
"""
|
|
175
|
+
return self._class_index.get(name)
|
|
176
|
+
|
|
177
|
+
def find_classes(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
name: str | None = None,
|
|
181
|
+
extends: str | None = None,
|
|
182
|
+
implements: str | None = None,
|
|
183
|
+
package: str | None = None,
|
|
184
|
+
is_interface: bool | None = None,
|
|
185
|
+
) -> list[ClassInfo]:
|
|
186
|
+
"""Find classes matching the given criteria.
|
|
187
|
+
|
|
188
|
+
All criteria are AND-combined.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
name: Substring match on class name.
|
|
192
|
+
extends: Exact match on superclass name.
|
|
193
|
+
implements: Exact match on one of the interface names.
|
|
194
|
+
package: Exact match on package name.
|
|
195
|
+
is_interface: Filter by interface flag.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
List of matching ClassInfo objects.
|
|
199
|
+
"""
|
|
200
|
+
results = self._classes
|
|
201
|
+
if name is not None:
|
|
202
|
+
results = [c for c in results if name in c.name]
|
|
203
|
+
if extends is not None:
|
|
204
|
+
results = [c for c in results if c.super_name == extends]
|
|
205
|
+
if implements is not None:
|
|
206
|
+
results = [c for c in results if implements in c.interfaces]
|
|
207
|
+
if package is not None:
|
|
208
|
+
results = [c for c in results if c.package == package]
|
|
209
|
+
if is_interface is not None:
|
|
210
|
+
results = [c for c in results if c.is_interface == is_interface]
|
|
211
|
+
return results
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def class_count(self) -> int:
|
|
215
|
+
return len(self._classes)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def interface_count(self) -> int:
|
|
219
|
+
return sum(1 for c in self._classes if c.is_interface)
|
|
220
|
+
|
|
221
|
+
def summary(self) -> str:
|
|
222
|
+
"""Return a human-readable summary of the workspace."""
|
|
223
|
+
lines = [f"Workspace: {len(self.resources)} resource(s)"]
|
|
224
|
+
for res in self.resources:
|
|
225
|
+
lines.append(
|
|
226
|
+
f" {res.path}: {res.class_count} classes, "
|
|
227
|
+
f"{res.method_count} methods, {res.string_count} strings")
|
|
228
|
+
lines.append(
|
|
229
|
+
f"Total: {self.class_count} classes, "
|
|
230
|
+
f"{self.interface_count} interfaces, "
|
|
231
|
+
f"{len(self.packages)} packages")
|
|
232
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyflashkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: SWF/ABC toolkit for parsing, analyzing, and manipulating Flash files and AVM2 bytecode
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Topic :: Software Development :: Disassemblers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# flashkit
|
|
23
|
+
|
|
24
|
+
Parse, analyze, and manipulate Adobe Flash SWF files and AVM2 bytecode.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from flashkit.workspace import Workspace
|
|
36
|
+
|
|
37
|
+
ws = Workspace()
|
|
38
|
+
ws.load_swf("application.swf")
|
|
39
|
+
|
|
40
|
+
# Find all classes extending Sprite
|
|
41
|
+
for cls in ws.find_classes(extends="Sprite"):
|
|
42
|
+
print(f"{cls.qualified_name} — {len(cls.fields)} fields, {len(cls.methods)} methods")
|
|
43
|
+
|
|
44
|
+
# Inspect a specific class
|
|
45
|
+
player = ws.get_class("PlayerManager")
|
|
46
|
+
print(player.super_name) # "EventDispatcher"
|
|
47
|
+
print(player.interfaces) # ["IDisposable", "ITickable"]
|
|
48
|
+
print(player.fields[0].name, player.fields[0].type_name) # "mHealth", "Number"
|
|
49
|
+
|
|
50
|
+
# Search strings used in bytecode
|
|
51
|
+
from flashkit.analysis import StringIndex
|
|
52
|
+
strings = StringIndex.from_workspace(ws)
|
|
53
|
+
for s in strings.search("config"):
|
|
54
|
+
print(s)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## CLI
|
|
60
|
+
|
|
61
|
+
### `flashkit info`
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ flashkit info application.swf
|
|
65
|
+
File: application.swf
|
|
66
|
+
Format: SWF
|
|
67
|
+
SWF version: 40
|
|
68
|
+
Tags: 142
|
|
69
|
+
ABC blocks: 1
|
|
70
|
+
Classes: 823
|
|
71
|
+
Methods: 14210
|
|
72
|
+
Strings: 35482
|
|
73
|
+
Packages: 47
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `flashkit classes`
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
flashkit classes app.swf # all classes
|
|
80
|
+
flashkit classes app.swf -s Manager # search by name
|
|
81
|
+
flashkit classes app.swf -p com.game # filter by package
|
|
82
|
+
flashkit classes app.swf -e Sprite # filter by superclass
|
|
83
|
+
flashkit classes app.swf -i # interfaces only
|
|
84
|
+
flashkit classes app.swf -v # verbose output
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `flashkit class`
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
$ flashkit class application.swf PlayerManager
|
|
91
|
+
PlayerManager
|
|
92
|
+
Package: com.game
|
|
93
|
+
Extends: EventDispatcher
|
|
94
|
+
Implements: IDisposable, ITickable
|
|
95
|
+
|
|
96
|
+
Instance Fields (3)
|
|
97
|
+
mHealth: Number
|
|
98
|
+
mName: String
|
|
99
|
+
mLevel: int
|
|
100
|
+
|
|
101
|
+
Instance Methods (5)
|
|
102
|
+
init(): void
|
|
103
|
+
get name(): String
|
|
104
|
+
set name(value: String): void
|
|
105
|
+
takeDamage(amount: Number): void
|
|
106
|
+
serialize(): ByteArray
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `flashkit strings`
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
flashkit strings app.swf # list all
|
|
113
|
+
flashkit strings app.swf -s config # search
|
|
114
|
+
flashkit strings app.swf -s config -v # with usage locations
|
|
115
|
+
flashkit strings app.swf -s "\\d+" -r # regex
|
|
116
|
+
flashkit strings app.swf -c # classify (URLs, debug)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `flashkit tags`
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
flashkit tags app.swf
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `flashkit disasm`
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
flashkit disasm app.swf --class PlayerManager
|
|
129
|
+
flashkit disasm app.swf --method-index 42
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### `flashkit tree`
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
flashkit tree app.swf BaseEntity # show descendants
|
|
136
|
+
flashkit tree app.swf PlayerManager -a # show ancestors
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `flashkit callers` / `flashkit callees`
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
flashkit callers app.swf toString
|
|
143
|
+
flashkit callees app.swf PlayerManager.init
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `flashkit refs`
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
flashkit refs app.swf Point
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `flashkit packages` / `flashkit extract` / `flashkit build`
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
flashkit packages app.swf # list packages
|
|
156
|
+
flashkit extract app.swf -o ./output # extract ABC blocks
|
|
157
|
+
flashkit build app.swf -o rebuilt.swf # rebuild (compressed)
|
|
158
|
+
flashkit build app.swf -o out.swf -d # rebuild (decompressed)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Library
|
|
164
|
+
|
|
165
|
+
### Load and query
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from flashkit.workspace import Workspace
|
|
169
|
+
|
|
170
|
+
ws = Workspace()
|
|
171
|
+
ws.load_swf("application.swf")
|
|
172
|
+
ws.load_swz("module.swz")
|
|
173
|
+
|
|
174
|
+
print(ws.summary())
|
|
175
|
+
|
|
176
|
+
cls = ws.get_class("MyClass")
|
|
177
|
+
print(cls.name, cls.super_name, cls.interfaces)
|
|
178
|
+
print(cls.fields) # list of FieldInfo
|
|
179
|
+
print(cls.methods) # list of MethodInfoResolved
|
|
180
|
+
|
|
181
|
+
ws.find_classes(extends="Sprite")
|
|
182
|
+
ws.find_classes(package="com.example", is_interface=True)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Analysis
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from flashkit.analysis import InheritanceGraph, CallGraph, StringIndex
|
|
189
|
+
|
|
190
|
+
graph = InheritanceGraph.from_classes(ws.classes)
|
|
191
|
+
graph.get_children("BaseEntity")
|
|
192
|
+
graph.get_all_parents("MyClass")
|
|
193
|
+
graph.get_implementors("ISerializable")
|
|
194
|
+
|
|
195
|
+
calls = CallGraph.from_workspace(ws)
|
|
196
|
+
calls.get_callers("toString")
|
|
197
|
+
calls.get_callees("MyClass.init")
|
|
198
|
+
|
|
199
|
+
strings = StringIndex.from_workspace(ws)
|
|
200
|
+
strings.search("config")
|
|
201
|
+
strings.url_strings()
|
|
202
|
+
strings.classes_using_string("http://example.com")
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
<details>
|
|
206
|
+
<summary><strong>Parse SWF and ABC directly</strong></summary>
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from flashkit.swf import parse_swf, TAG_DO_ABC2
|
|
210
|
+
from flashkit.abc import parse_abc, serialize_abc
|
|
211
|
+
|
|
212
|
+
header, tags, version, length = parse_swf(swf_bytes)
|
|
213
|
+
|
|
214
|
+
for tag in tags:
|
|
215
|
+
if tag.tag_type == TAG_DO_ABC2:
|
|
216
|
+
null_idx = tag.payload.index(0, 4)
|
|
217
|
+
abc = parse_abc(tag.payload[null_idx + 1:])
|
|
218
|
+
print(f"{len(abc.instances)} classes, {len(abc.methods)} methods")
|
|
219
|
+
|
|
220
|
+
# Round-trip fidelity: serialize(parse(data)) == data
|
|
221
|
+
assert serialize_abc(abc) == tag.payload[null_idx + 1:]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
<details>
|
|
227
|
+
<summary><strong>Build SWF programmatically</strong></summary>
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from flashkit.abc import AbcBuilder, serialize_abc
|
|
231
|
+
from flashkit.swf import SwfBuilder
|
|
232
|
+
|
|
233
|
+
b = AbcBuilder()
|
|
234
|
+
b.simple_class("Player", package="com.game",
|
|
235
|
+
fields=[("hp", "int"), ("name", "String")])
|
|
236
|
+
b.script()
|
|
237
|
+
abc_bytes = serialize_abc(b.build())
|
|
238
|
+
|
|
239
|
+
swf = SwfBuilder(version=40, width=800, height=600, fps=30)
|
|
240
|
+
swf.add_abc("GameCode", abc_bytes)
|
|
241
|
+
swf_bytes = swf.build(compress=True)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
</details>
|
|
245
|
+
|
|
246
|
+
<details>
|
|
247
|
+
<summary><strong>Disassemble method bodies</strong></summary>
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from flashkit.abc import decode_instructions
|
|
251
|
+
|
|
252
|
+
for body in abc.method_bodies:
|
|
253
|
+
for instr in decode_instructions(body.code):
|
|
254
|
+
print(f"0x{instr.offset:04X} {instr.mnemonic} {instr.operands}")
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
</details>
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Project structure
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
flashkit/
|
|
265
|
+
cli/ CLI (one module per command)
|
|
266
|
+
swf/ SWF container (parse, build, tags)
|
|
267
|
+
abc/ AVM2 bytecode (parse, write, disasm, builder)
|
|
268
|
+
info/ Resolved class model (ClassInfo, FieldInfo, MethodInfo)
|
|
269
|
+
workspace/ File loading and class index
|
|
270
|
+
analysis/ Inheritance, call graph, references, strings
|
|
271
|
+
search/ Unified query engine
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## References
|
|
275
|
+
|
|
276
|
+
- [AVM2 Overview (Adobe)](https://www.adobe.com/content/dam/acom/en/devnet/pdf/avm2overview.pdf)
|
|
277
|
+
- [SWF File Format Specification](https://open-flash.github.io/mirrors/swf-spec-19.pdf)
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT
|