pyflashkit 1.0.0__tar.gz → 1.2.0__tar.gz
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.
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/PKG-INFO +56 -22
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/README.md +55 -21
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/__init__.py +5 -3
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/__init__.py +4 -1
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/builder.py +19 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/disasm.py +235 -1
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/types.py +12 -12
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/__init__.py +28 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/call_graph.py +58 -46
- pyflashkit-1.2.0/flashkit/analysis/class_graph.py +270 -0
- pyflashkit-1.2.0/flashkit/analysis/field_access.py +426 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/inheritance.py +19 -1
- pyflashkit-1.2.0/flashkit/analysis/method_fingerprint.py +378 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/references.py +52 -76
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/strings.py +24 -22
- pyflashkit-1.2.0/flashkit/analysis/unified.py +182 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/__init__.py +2 -1
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/callees.py +1 -4
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/callers.py +1 -4
- pyflashkit-1.2.0/flashkit/cli/field_access.py +101 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/refs.py +1 -4
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/strings.py +7 -9
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/tree.py +4 -8
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/class_info.py +160 -2
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/member_info.py +109 -4
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/package_info.py +1 -1
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/tags.py +1 -1
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/workspace/resource.py +1 -1
- pyflashkit-1.2.0/flashkit/workspace/workspace.py +771 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/PKG-INFO +56 -22
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/SOURCES.txt +12 -5
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyproject.toml +1 -1
- pyflashkit-1.2.0/tests/analysis/test_class_graph.py +10 -0
- pyflashkit-1.2.0/tests/analysis/test_field_access.py +461 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_inheritance.py +9 -0
- pyflashkit-1.2.0/tests/analysis/test_type_hints.py +38 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/cli/test_cli.py +1 -1
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/conftest.py +9 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/info/test_class_info.py +61 -0
- pyflashkit-1.2.0/tests/info/test_member_info.py +473 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/test_integration.py +9 -11
- pyflashkit-1.2.0/tests/test_public_api.py +41 -0
- pyflashkit-1.2.0/tests/workspace/test_workspace.py +414 -0
- pyflashkit-1.2.0/tests/workspace/test_workspace_properties.py +44 -0
- pyflashkit-1.0.0/flashkit/search/__init__.py +0 -16
- pyflashkit-1.0.0/flashkit/search/search.py +0 -456
- pyflashkit-1.0.0/flashkit/workspace/workspace.py +0 -232
- pyflashkit-1.0.0/tests/search/test_search.py +0 -227
- pyflashkit-1.0.0/tests/workspace/__init__.py +0 -0
- pyflashkit-1.0.0/tests/workspace/test_workspace.py +0 -206
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/.github/workflows/ci.yml +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/.github/workflows/release.yml +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/.gitignore +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/CONTRIBUTING.md +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/LICENSE +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/constants.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/parser.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/writer.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/_util.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/build.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/class_cmd.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/classes.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/disasm.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/extract.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/info.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/packages.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/tags.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/errors.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/builder.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/parser.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/workspace/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/dependency_links.txt +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/entry_points.txt +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/requires.txt +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/top_level.txt +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/setup.cfg +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_builder.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_disasm.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_parser.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_writer.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_call_graph.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_references.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_strings.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/cli/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/info/__init__.py +0 -0
- {pyflashkit-1.0.0/tests/search → pyflashkit-1.2.0/tests/swf}/__init__.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/swf/test_builder.py +0 -0
- {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/swf/test_parser.py +0 -0
- {pyflashkit-1.0.0/tests/swf → pyflashkit-1.2.0/tests/workspace}/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyflashkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: SWF/ABC toolkit for parsing, analyzing, and manipulating Flash files and AVM2 bytecode
|
|
5
5
|
License: MIT
|
|
6
6
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -26,6 +26,14 @@ Parse, analyze, and manipulate Adobe Flash SWF files and AVM2 bytecode.
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
+
pip install pyflashkit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or from source:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/bitalizer/pyflashkit.git
|
|
36
|
+
cd pyflashkit
|
|
29
37
|
pip install -e .
|
|
30
38
|
```
|
|
31
39
|
|
|
@@ -48,9 +56,7 @@ print(player.interfaces) # ["IDisposable", "ITickable"]
|
|
|
48
56
|
print(player.fields[0].name, player.fields[0].type_name) # "mHealth", "Number"
|
|
49
57
|
|
|
50
58
|
# Search strings used in bytecode
|
|
51
|
-
|
|
52
|
-
strings = StringIndex.from_workspace(ws)
|
|
53
|
-
for s in strings.search("config"):
|
|
59
|
+
for s in ws.search_strings("config"):
|
|
54
60
|
print(s)
|
|
55
61
|
```
|
|
56
62
|
|
|
@@ -149,6 +155,15 @@ flashkit callees app.swf PlayerManager.init
|
|
|
149
155
|
flashkit refs app.swf Point
|
|
150
156
|
```
|
|
151
157
|
|
|
158
|
+
### `flashkit fields`
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
flashkit fields app.swf PlayerManager # field access summary (R/W counts)
|
|
162
|
+
flashkit fields app.swf PlayerManager -c # constructor assignments in order
|
|
163
|
+
flashkit fields app.swf PlayerManager -f mHealth # who reads/writes a specific field
|
|
164
|
+
flashkit fields app.swf PlayerManager -m takeDamage # what fields a method accesses
|
|
165
|
+
```
|
|
166
|
+
|
|
152
167
|
### `flashkit packages` / `flashkit extract` / `flashkit build`
|
|
153
168
|
|
|
154
169
|
```bash
|
|
@@ -182,24 +197,44 @@ ws.find_classes(extends="Sprite")
|
|
|
182
197
|
ws.find_classes(package="com.example", is_interface=True)
|
|
183
198
|
```
|
|
184
199
|
|
|
185
|
-
###
|
|
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")
|
|
200
|
+
### Search and analysis
|
|
194
201
|
|
|
195
|
-
|
|
196
|
-
calls.get_callers("toString")
|
|
197
|
-
calls.get_callees("MyClass.init")
|
|
202
|
+
All analysis is accessed directly through the Workspace — no separate imports needed.
|
|
198
203
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
```python
|
|
205
|
+
# Inheritance
|
|
206
|
+
ws.get_subclasses("BaseEntity")
|
|
207
|
+
ws.get_descendants("BaseEntity") # transitive
|
|
208
|
+
ws.get_ancestors("PlayerManager")
|
|
209
|
+
ws.get_implementors("ISerializable")
|
|
210
|
+
|
|
211
|
+
# Call graph
|
|
212
|
+
ws.callers("toString")
|
|
213
|
+
ws.callees("PlayerManager.init")
|
|
214
|
+
|
|
215
|
+
# References
|
|
216
|
+
ws.references_to("Point")
|
|
217
|
+
ws.references_from("PlayerManager")
|
|
218
|
+
ws.find_instantiators("Point")
|
|
219
|
+
ws.find_type_users("ByteArray")
|
|
220
|
+
|
|
221
|
+
# Strings
|
|
222
|
+
ws.search_strings("config")
|
|
223
|
+
ws.classes_using_string("http://example.com")
|
|
224
|
+
ws.strings_in_class("PlayerManager")
|
|
225
|
+
|
|
226
|
+
# Field access
|
|
227
|
+
ws.field_writers("PlayerManager", "mHealth")
|
|
228
|
+
ws.field_readers("PlayerManager", "mHealth")
|
|
229
|
+
ws.fields_written_by("PlayerManager", "takeDamage")
|
|
230
|
+
ws.fields_read_by("PlayerManager", "takeDamage")
|
|
231
|
+
ws.constructor_assignments("PlayerManager")
|
|
232
|
+
ws.field_access_summary("PlayerManager")
|
|
233
|
+
|
|
234
|
+
# Structural
|
|
235
|
+
ws.find_classes_with_field_type("ByteArray")
|
|
236
|
+
ws.find_methods(return_type="String", name="get")
|
|
237
|
+
ws.find_fields(type_name="int")
|
|
203
238
|
```
|
|
204
239
|
|
|
205
240
|
<details>
|
|
@@ -267,8 +302,7 @@ flashkit/
|
|
|
267
302
|
abc/ AVM2 bytecode (parse, write, disasm, builder)
|
|
268
303
|
info/ Resolved class model (ClassInfo, FieldInfo, MethodInfo)
|
|
269
304
|
workspace/ File loading and class index
|
|
270
|
-
analysis/ Inheritance, call graph, references, strings
|
|
271
|
-
search/ Unified query engine
|
|
305
|
+
analysis/ Inheritance, call graph, references, strings, field access
|
|
272
306
|
```
|
|
273
307
|
|
|
274
308
|
## References
|
|
@@ -5,6 +5,14 @@ Parse, analyze, and manipulate Adobe Flash SWF files and AVM2 bytecode.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
pip install pyflashkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/bitalizer/pyflashkit.git
|
|
15
|
+
cd pyflashkit
|
|
8
16
|
pip install -e .
|
|
9
17
|
```
|
|
10
18
|
|
|
@@ -27,9 +35,7 @@ print(player.interfaces) # ["IDisposable", "ITickable"]
|
|
|
27
35
|
print(player.fields[0].name, player.fields[0].type_name) # "mHealth", "Number"
|
|
28
36
|
|
|
29
37
|
# Search strings used in bytecode
|
|
30
|
-
|
|
31
|
-
strings = StringIndex.from_workspace(ws)
|
|
32
|
-
for s in strings.search("config"):
|
|
38
|
+
for s in ws.search_strings("config"):
|
|
33
39
|
print(s)
|
|
34
40
|
```
|
|
35
41
|
|
|
@@ -128,6 +134,15 @@ flashkit callees app.swf PlayerManager.init
|
|
|
128
134
|
flashkit refs app.swf Point
|
|
129
135
|
```
|
|
130
136
|
|
|
137
|
+
### `flashkit fields`
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
flashkit fields app.swf PlayerManager # field access summary (R/W counts)
|
|
141
|
+
flashkit fields app.swf PlayerManager -c # constructor assignments in order
|
|
142
|
+
flashkit fields app.swf PlayerManager -f mHealth # who reads/writes a specific field
|
|
143
|
+
flashkit fields app.swf PlayerManager -m takeDamage # what fields a method accesses
|
|
144
|
+
```
|
|
145
|
+
|
|
131
146
|
### `flashkit packages` / `flashkit extract` / `flashkit build`
|
|
132
147
|
|
|
133
148
|
```bash
|
|
@@ -161,24 +176,44 @@ ws.find_classes(extends="Sprite")
|
|
|
161
176
|
ws.find_classes(package="com.example", is_interface=True)
|
|
162
177
|
```
|
|
163
178
|
|
|
164
|
-
###
|
|
165
|
-
|
|
166
|
-
```python
|
|
167
|
-
from flashkit.analysis import InheritanceGraph, CallGraph, StringIndex
|
|
168
|
-
|
|
169
|
-
graph = InheritanceGraph.from_classes(ws.classes)
|
|
170
|
-
graph.get_children("BaseEntity")
|
|
171
|
-
graph.get_all_parents("MyClass")
|
|
172
|
-
graph.get_implementors("ISerializable")
|
|
179
|
+
### Search and analysis
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
calls.get_callers("toString")
|
|
176
|
-
calls.get_callees("MyClass.init")
|
|
181
|
+
All analysis is accessed directly through the Workspace — no separate imports needed.
|
|
177
182
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
```python
|
|
184
|
+
# Inheritance
|
|
185
|
+
ws.get_subclasses("BaseEntity")
|
|
186
|
+
ws.get_descendants("BaseEntity") # transitive
|
|
187
|
+
ws.get_ancestors("PlayerManager")
|
|
188
|
+
ws.get_implementors("ISerializable")
|
|
189
|
+
|
|
190
|
+
# Call graph
|
|
191
|
+
ws.callers("toString")
|
|
192
|
+
ws.callees("PlayerManager.init")
|
|
193
|
+
|
|
194
|
+
# References
|
|
195
|
+
ws.references_to("Point")
|
|
196
|
+
ws.references_from("PlayerManager")
|
|
197
|
+
ws.find_instantiators("Point")
|
|
198
|
+
ws.find_type_users("ByteArray")
|
|
199
|
+
|
|
200
|
+
# Strings
|
|
201
|
+
ws.search_strings("config")
|
|
202
|
+
ws.classes_using_string("http://example.com")
|
|
203
|
+
ws.strings_in_class("PlayerManager")
|
|
204
|
+
|
|
205
|
+
# Field access
|
|
206
|
+
ws.field_writers("PlayerManager", "mHealth")
|
|
207
|
+
ws.field_readers("PlayerManager", "mHealth")
|
|
208
|
+
ws.fields_written_by("PlayerManager", "takeDamage")
|
|
209
|
+
ws.fields_read_by("PlayerManager", "takeDamage")
|
|
210
|
+
ws.constructor_assignments("PlayerManager")
|
|
211
|
+
ws.field_access_summary("PlayerManager")
|
|
212
|
+
|
|
213
|
+
# Structural
|
|
214
|
+
ws.find_classes_with_field_type("ByteArray")
|
|
215
|
+
ws.find_methods(return_type="String", name="get")
|
|
216
|
+
ws.find_fields(type_name="int")
|
|
182
217
|
```
|
|
183
218
|
|
|
184
219
|
<details>
|
|
@@ -246,8 +281,7 @@ flashkit/
|
|
|
246
281
|
abc/ AVM2 bytecode (parse, write, disasm, builder)
|
|
247
282
|
info/ Resolved class model (ClassInfo, FieldInfo, MethodInfo)
|
|
248
283
|
workspace/ File loading and class index
|
|
249
|
-
analysis/ Inheritance, call graph, references, strings
|
|
250
|
-
search/ Unified query engine
|
|
284
|
+
analysis/ Inheritance, call graph, references, strings, field access
|
|
251
285
|
```
|
|
252
286
|
|
|
253
287
|
## References
|
|
@@ -12,8 +12,6 @@ Packages:
|
|
|
12
12
|
info: Rich resolved model (ClassInfo, FieldInfo, MethodInfo).
|
|
13
13
|
workspace: Loaded binary workspace (SWF/SWZ resources).
|
|
14
14
|
analysis: Inheritance graph, call graph, references, strings.
|
|
15
|
-
search: Query engine for workspace content.
|
|
16
|
-
|
|
17
15
|
Quick start::
|
|
18
16
|
|
|
19
17
|
from flashkit import parse_swf, parse_abc, serialize_abc
|
|
@@ -23,7 +21,7 @@ Quick start::
|
|
|
23
21
|
output = serialize_abc(abc)
|
|
24
22
|
"""
|
|
25
23
|
|
|
26
|
-
__version__ = "1.
|
|
24
|
+
__version__ = "1.2.0"
|
|
27
25
|
|
|
28
26
|
from .errors import (
|
|
29
27
|
FlashkitError, ParseError, SWFParseError,
|
|
@@ -34,6 +32,8 @@ from .swf.builder import rebuild_swf, make_doabc2_tag
|
|
|
34
32
|
from .abc.parser import parse_abc
|
|
35
33
|
from .abc.writer import serialize_abc
|
|
36
34
|
from .abc.types import AbcFile
|
|
35
|
+
from .workspace.workspace import Workspace
|
|
36
|
+
from .info.class_info import ClassInfo
|
|
37
37
|
|
|
38
38
|
__all__ = [
|
|
39
39
|
"__version__",
|
|
@@ -51,4 +51,6 @@ __all__ = [
|
|
|
51
51
|
"parse_abc",
|
|
52
52
|
"serialize_abc",
|
|
53
53
|
"AbcFile",
|
|
54
|
+
"Workspace",
|
|
55
|
+
"ClassInfo",
|
|
54
56
|
]
|
|
@@ -41,7 +41,7 @@ from .parser import (
|
|
|
41
41
|
read_d64,
|
|
42
42
|
)
|
|
43
43
|
from .writer import serialize_abc
|
|
44
|
-
from .disasm import Instruction, decode_instructions
|
|
44
|
+
from .disasm import Instruction, ResolvedInstruction, decode_instructions, resolve_instructions, scan_relevant_opcodes
|
|
45
45
|
from .builder import AbcBuilder
|
|
46
46
|
|
|
47
47
|
__all__ = [
|
|
@@ -73,7 +73,10 @@ __all__ = [
|
|
|
73
73
|
"serialize_abc",
|
|
74
74
|
# Disassembler
|
|
75
75
|
"Instruction",
|
|
76
|
+
"ResolvedInstruction",
|
|
76
77
|
"decode_instructions",
|
|
78
|
+
"resolve_instructions",
|
|
79
|
+
"scan_relevant_opcodes",
|
|
77
80
|
# Builder
|
|
78
81
|
"AbcBuilder",
|
|
79
82
|
]
|
|
@@ -311,6 +311,25 @@ class AbcBuilder:
|
|
|
311
311
|
kind=CONSTANT_RTQName, name=name))
|
|
312
312
|
return idx
|
|
313
313
|
|
|
314
|
+
def typename(self, base: int, params: list[int]) -> int:
|
|
315
|
+
"""Add a TypeName (parameterized type) multiname, e.g. Vector.<int>.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
base: Multiname pool index for the base type (e.g. Vector).
|
|
319
|
+
params: List of multiname pool indices for type parameters.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Multiname pool index.
|
|
323
|
+
"""
|
|
324
|
+
param_bytes = bytearray()
|
|
325
|
+
for p in params:
|
|
326
|
+
param_bytes += write_u30(p)
|
|
327
|
+
idx = len(self._multiname_pool)
|
|
328
|
+
self._multiname_pool.append(MultinameInfo(
|
|
329
|
+
kind=CONSTANT_TypeName, ns=base, name=len(params),
|
|
330
|
+
data=bytes(param_bytes)))
|
|
331
|
+
return idx
|
|
332
|
+
|
|
314
333
|
# ── Methods ────────────────────────────────────────────────────────
|
|
315
334
|
|
|
316
335
|
def method(
|
|
@@ -25,7 +25,7 @@ from .constants import *
|
|
|
25
25
|
log = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
@dataclass
|
|
28
|
+
@dataclass(slots=True)
|
|
29
29
|
class Instruction:
|
|
30
30
|
"""A single decoded AVM2 instruction.
|
|
31
31
|
|
|
@@ -43,6 +43,24 @@ class Instruction:
|
|
|
43
43
|
size: int = 1
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class ResolvedInstruction:
|
|
48
|
+
"""An AVM2 instruction with operands resolved to readable names.
|
|
49
|
+
|
|
50
|
+
Created by ``resolve_instructions()`` from raw ``Instruction`` objects.
|
|
51
|
+
Multiname indices become class/field/method names, string indices become
|
|
52
|
+
quoted literals, int/uint/double indices become numeric values.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
offset: Byte offset in the method body.
|
|
56
|
+
mnemonic: Opcode name (e.g. ``"getproperty"``).
|
|
57
|
+
operands: Human-readable operand strings.
|
|
58
|
+
"""
|
|
59
|
+
offset: int
|
|
60
|
+
mnemonic: str
|
|
61
|
+
operands: list[str] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
|
|
46
64
|
# ── Opcode table ────────────────────────────────────────────────────────────
|
|
47
65
|
# Maps opcode → (mnemonic, operand_format)
|
|
48
66
|
# Operand formats:
|
|
@@ -275,6 +293,116 @@ def _build_lookup() -> dict[int, tuple[str, str]]:
|
|
|
275
293
|
_LOOKUP = _build_lookup()
|
|
276
294
|
|
|
277
295
|
|
|
296
|
+
# ── Fast operand-format table for the lightweight scanner ──────────────────
|
|
297
|
+
# Maps every known opcode to an integer encoding its operand format:
|
|
298
|
+
# 0 = none, 1 = u8, 2 = u30, 3 = u30u30, 4 = s24, 5 = lookupswitch, 6 = debug
|
|
299
|
+
_FMT_CODE = {"": 0, "u8": 1, "u30": 2, "u30u30": 3, "s24": 4, "u30u8": 3}
|
|
300
|
+
|
|
301
|
+
def _build_skip_table() -> list[int]:
|
|
302
|
+
"""Build a 256-entry table: opcode → operand format code.
|
|
303
|
+
|
|
304
|
+
Unknown opcodes get format code 0xFF (sentinel for "stop scanning").
|
|
305
|
+
"""
|
|
306
|
+
tbl = [0xFF] * 256
|
|
307
|
+
for op, (_, fmt) in _LOOKUP.items():
|
|
308
|
+
if fmt == "special":
|
|
309
|
+
# OP_lookupswitch=5, OP_debug=6
|
|
310
|
+
tbl[op] = 5 if op == OP_lookupswitch else 6
|
|
311
|
+
else:
|
|
312
|
+
tbl[op] = _FMT_CODE.get(fmt, 0)
|
|
313
|
+
return tbl
|
|
314
|
+
|
|
315
|
+
_SKIP_TABLE = _build_skip_table()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _skip_u30(data: bytes, off: int) -> int:
|
|
319
|
+
"""Advance past a u30 without decoding its value."""
|
|
320
|
+
for _ in range(5):
|
|
321
|
+
if (data[off] & 0x80) == 0:
|
|
322
|
+
return off + 1
|
|
323
|
+
off += 1
|
|
324
|
+
return off
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def scan_relevant_opcodes(
|
|
328
|
+
code: bytes,
|
|
329
|
+
opcodes: frozenset[int],
|
|
330
|
+
) -> list[tuple[int, int, int]]:
|
|
331
|
+
"""Lightweight bytecode scanner that only decodes opcodes of interest.
|
|
332
|
+
|
|
333
|
+
Walks the bytecode stream, skipping operands for irrelevant opcodes
|
|
334
|
+
via a precomputed table lookup. For opcodes in *opcodes*, decodes
|
|
335
|
+
the first u30 operand and records the hit.
|
|
336
|
+
|
|
337
|
+
This avoids creating Instruction objects, mnemonic strings, and
|
|
338
|
+
operand lists for the vast majority of instructions.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
code: Raw bytecode bytes (from MethodBodyInfo.code).
|
|
342
|
+
opcodes: Set of opcode values to capture.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
List of ``(offset, opcode, first_operand)`` tuples for matched
|
|
346
|
+
instructions. The first operand is always the first u30 in the
|
|
347
|
+
instruction's operand stream (valid for u30 and u30u30 formats).
|
|
348
|
+
"""
|
|
349
|
+
results: list[tuple[int, int, int]] = []
|
|
350
|
+
off = 0
|
|
351
|
+
code_len = len(code)
|
|
352
|
+
skip_table = _SKIP_TABLE
|
|
353
|
+
|
|
354
|
+
while off < code_len:
|
|
355
|
+
start = off
|
|
356
|
+
op = code[off]
|
|
357
|
+
off += 1
|
|
358
|
+
fmt = skip_table[op]
|
|
359
|
+
|
|
360
|
+
if op in opcodes:
|
|
361
|
+
# All relevant opcodes have u30 or u30u30 format — decode first u30
|
|
362
|
+
try:
|
|
363
|
+
val, off = read_u30(code, off)
|
|
364
|
+
except (IndexError, ValueError):
|
|
365
|
+
break
|
|
366
|
+
results.append((start, op, val))
|
|
367
|
+
# If u30u30, skip the second u30
|
|
368
|
+
if fmt == 3:
|
|
369
|
+
try:
|
|
370
|
+
off = _skip_u30(code, off)
|
|
371
|
+
except IndexError:
|
|
372
|
+
break
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
# Skip operands for irrelevant opcodes
|
|
376
|
+
try:
|
|
377
|
+
if fmt == 0: # no operands
|
|
378
|
+
pass
|
|
379
|
+
elif fmt == 1: # u8
|
|
380
|
+
off += 1
|
|
381
|
+
elif fmt == 2: # u30
|
|
382
|
+
off = _skip_u30(code, off)
|
|
383
|
+
elif fmt == 3: # u30u30
|
|
384
|
+
off = _skip_u30(code, off)
|
|
385
|
+
off = _skip_u30(code, off)
|
|
386
|
+
elif fmt == 4: # s24
|
|
387
|
+
off += 3
|
|
388
|
+
elif fmt == 5: # lookupswitch
|
|
389
|
+
off += 3 # default s24
|
|
390
|
+
case_count, off = read_u30(code, off)
|
|
391
|
+
off += (case_count + 1) * 3 # case s24s
|
|
392
|
+
elif fmt == 6: # debug
|
|
393
|
+
off += 1 # debug_type u8
|
|
394
|
+
off = _skip_u30(code, off) # index u30
|
|
395
|
+
off += 1 # reg u8
|
|
396
|
+
off = _skip_u30(code, off) # extra u30
|
|
397
|
+
else:
|
|
398
|
+
# Unknown opcode — can't determine size, bail out
|
|
399
|
+
break
|
|
400
|
+
except (IndexError, ValueError):
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
return results
|
|
404
|
+
|
|
405
|
+
|
|
278
406
|
def decode_instructions(code: bytes,
|
|
279
407
|
strict: bool = False) -> list[Instruction]:
|
|
280
408
|
"""Decode an AVM2 bytecode stream into a list of instructions.
|
|
@@ -362,3 +490,109 @@ def decode_instructions(code: bytes,
|
|
|
362
490
|
operands=operands, size=off - start))
|
|
363
491
|
|
|
364
492
|
return instructions
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# ── Opcodes grouped by operand resolution type ─────────────────────────────
|
|
496
|
+
# First operand is a multiname pool index
|
|
497
|
+
_MULTINAME_FIRST = frozenset({
|
|
498
|
+
OP_getproperty, OP_setproperty, OP_initproperty,
|
|
499
|
+
OP_getlex, OP_findpropstrict,
|
|
500
|
+
OP_callproperty, OP_callpropvoid, OP_constructprop,
|
|
501
|
+
OP_coerce,
|
|
502
|
+
# Extra opcodes (from _EXTRA_OPCODES)
|
|
503
|
+
0x04, # getsuper
|
|
504
|
+
0x05, # setsuper
|
|
505
|
+
0x5E, # findproperty
|
|
506
|
+
0x45, # callsuper
|
|
507
|
+
0x4C, # callproplex
|
|
508
|
+
0x4E, # callsupervoid
|
|
509
|
+
0x59, # getdescendants
|
|
510
|
+
0x6A, # deleteproperty
|
|
511
|
+
0x80, # coerce
|
|
512
|
+
0x86, # astype
|
|
513
|
+
0xB2, # istype
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
# First operand is a string pool index
|
|
517
|
+
_STRING_FIRST = frozenset({OP_pushstring})
|
|
518
|
+
|
|
519
|
+
# First operand is an int pool index
|
|
520
|
+
_INT_FIRST = frozenset({OP_pushint})
|
|
521
|
+
|
|
522
|
+
# First operand is a uint pool index
|
|
523
|
+
_UINT_FIRST = frozenset({OP_pushuint})
|
|
524
|
+
|
|
525
|
+
# First operand is a double pool index
|
|
526
|
+
_DOUBLE_FIRST = frozenset({OP_pushdouble})
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def resolve_instructions(
|
|
530
|
+
abc: "AbcFile",
|
|
531
|
+
instructions: list[Instruction],
|
|
532
|
+
) -> list[ResolvedInstruction]:
|
|
533
|
+
"""Resolve raw instruction operands to human-readable strings.
|
|
534
|
+
|
|
535
|
+
Multiname indices become names, string indices become quoted strings,
|
|
536
|
+
int/uint/double indices become literal values. Everything else stays
|
|
537
|
+
as raw numbers.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
abc: The AbcFile for constant pool lookups.
|
|
541
|
+
instructions: Raw decoded instructions.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
List of ResolvedInstruction with string operands.
|
|
545
|
+
"""
|
|
546
|
+
from .types import AbcFile as _AbcFile # noqa: F811
|
|
547
|
+
from ..info.member_info import resolve_multiname
|
|
548
|
+
|
|
549
|
+
resolved = []
|
|
550
|
+
for instr in instructions:
|
|
551
|
+
ops: list[str] = []
|
|
552
|
+
op = instr.opcode
|
|
553
|
+
|
|
554
|
+
for i, val in enumerate(instr.operands):
|
|
555
|
+
if i == 0 and op in _MULTINAME_FIRST:
|
|
556
|
+
try:
|
|
557
|
+
ops.append(resolve_multiname(abc, val))
|
|
558
|
+
except (IndexError, KeyError):
|
|
559
|
+
ops.append(f"multiname[{val}]")
|
|
560
|
+
elif i == 0 and op in _STRING_FIRST:
|
|
561
|
+
if 0 < val < len(abc.string_pool):
|
|
562
|
+
ops.append(f'"{abc.string_pool[val]}"')
|
|
563
|
+
else:
|
|
564
|
+
ops.append(f"string[{val}]")
|
|
565
|
+
elif i == 0 and op in _INT_FIRST:
|
|
566
|
+
if 0 < val < len(abc.int_pool):
|
|
567
|
+
ops.append(str(abc.int_pool[val]))
|
|
568
|
+
else:
|
|
569
|
+
ops.append(f"int[{val}]")
|
|
570
|
+
elif i == 0 and op in _UINT_FIRST:
|
|
571
|
+
if 0 < val < len(abc.uint_pool):
|
|
572
|
+
ops.append(str(abc.uint_pool[val]))
|
|
573
|
+
else:
|
|
574
|
+
ops.append(f"uint[{val}]")
|
|
575
|
+
elif i == 0 and op in _DOUBLE_FIRST:
|
|
576
|
+
if 0 < val < len(abc.double_pool):
|
|
577
|
+
ops.append(str(abc.double_pool[val]))
|
|
578
|
+
else:
|
|
579
|
+
ops.append(f"double[{val}]")
|
|
580
|
+
elif i == 0 and op == OP_newclass:
|
|
581
|
+
# val = class index
|
|
582
|
+
if 0 <= val < len(abc.instances):
|
|
583
|
+
try:
|
|
584
|
+
ops.append(resolve_multiname(abc, abc.instances[val].name))
|
|
585
|
+
except (IndexError, KeyError):
|
|
586
|
+
ops.append(f"class[{val}]")
|
|
587
|
+
else:
|
|
588
|
+
ops.append(f"class[{val}]")
|
|
589
|
+
else:
|
|
590
|
+
ops.append(str(val))
|
|
591
|
+
|
|
592
|
+
resolved.append(ResolvedInstruction(
|
|
593
|
+
offset=instr.offset,
|
|
594
|
+
mnemonic=instr.mnemonic,
|
|
595
|
+
operands=ops,
|
|
596
|
+
))
|
|
597
|
+
|
|
598
|
+
return resolved
|
|
@@ -16,7 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@dataclass
|
|
19
|
+
@dataclass(slots=True)
|
|
20
20
|
class NamespaceInfo:
|
|
21
21
|
"""A namespace entry in the constant pool.
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ class NamespaceInfo:
|
|
|
28
28
|
name: int
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
@dataclass
|
|
31
|
+
@dataclass(slots=True)
|
|
32
32
|
class NsSetInfo:
|
|
33
33
|
"""A namespace set — an unordered collection of namespaces.
|
|
34
34
|
|
|
@@ -40,7 +40,7 @@ class NsSetInfo:
|
|
|
40
40
|
namespaces: list[int]
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
@dataclass
|
|
43
|
+
@dataclass(slots=True)
|
|
44
44
|
class MultinameInfo:
|
|
45
45
|
"""A multiname entry in the constant pool.
|
|
46
46
|
|
|
@@ -71,7 +71,7 @@ class MultinameInfo:
|
|
|
71
71
|
ns_set: int = 0
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
@dataclass
|
|
74
|
+
@dataclass(slots=True)
|
|
75
75
|
class MethodInfo:
|
|
76
76
|
"""A method signature (not the body — see MethodBodyInfo).
|
|
77
77
|
|
|
@@ -93,7 +93,7 @@ class MethodInfo:
|
|
|
93
93
|
param_names: list[int] = field(default_factory=list)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
@dataclass
|
|
96
|
+
@dataclass(slots=True)
|
|
97
97
|
class MetadataInfo:
|
|
98
98
|
"""Metadata attached to traits (e.g. [SWF(width=800)]).
|
|
99
99
|
|
|
@@ -105,7 +105,7 @@ class MetadataInfo:
|
|
|
105
105
|
items: list[tuple]
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
@dataclass
|
|
108
|
+
@dataclass(slots=True)
|
|
109
109
|
class TraitInfo:
|
|
110
110
|
"""A trait (field, method, getter, setter, class, or const) on a class or script.
|
|
111
111
|
|
|
@@ -127,7 +127,7 @@ class TraitInfo:
|
|
|
127
127
|
data: bytes
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
@dataclass
|
|
130
|
+
@dataclass(slots=True)
|
|
131
131
|
class InstanceInfo:
|
|
132
132
|
"""An instance (non-static side) of a class definition.
|
|
133
133
|
|
|
@@ -152,7 +152,7 @@ class InstanceInfo:
|
|
|
152
152
|
traits: list[TraitInfo] = field(default_factory=list)
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
@dataclass
|
|
155
|
+
@dataclass(slots=True)
|
|
156
156
|
class ClassInfo:
|
|
157
157
|
"""The static side of a class definition.
|
|
158
158
|
|
|
@@ -166,7 +166,7 @@ class ClassInfo:
|
|
|
166
166
|
traits: list[TraitInfo] = field(default_factory=list)
|
|
167
167
|
|
|
168
168
|
|
|
169
|
-
@dataclass
|
|
169
|
+
@dataclass(slots=True)
|
|
170
170
|
class ScriptInfo:
|
|
171
171
|
"""A script entry point.
|
|
172
172
|
|
|
@@ -180,7 +180,7 @@ class ScriptInfo:
|
|
|
180
180
|
traits: list[TraitInfo] = field(default_factory=list)
|
|
181
181
|
|
|
182
182
|
|
|
183
|
-
@dataclass
|
|
183
|
+
@dataclass(slots=True)
|
|
184
184
|
class ExceptionInfo:
|
|
185
185
|
"""An exception handler within a method body.
|
|
186
186
|
|
|
@@ -198,7 +198,7 @@ class ExceptionInfo:
|
|
|
198
198
|
var_name: int
|
|
199
199
|
|
|
200
200
|
|
|
201
|
-
@dataclass
|
|
201
|
+
@dataclass(slots=True)
|
|
202
202
|
class MethodBodyInfo:
|
|
203
203
|
"""The bytecode body of a method.
|
|
204
204
|
|
|
@@ -222,7 +222,7 @@ class MethodBodyInfo:
|
|
|
222
222
|
traits: list[TraitInfo] = field(default_factory=list)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
-
@dataclass
|
|
225
|
+
@dataclass(slots=True)
|
|
226
226
|
class AbcFile:
|
|
227
227
|
"""A complete ABC (ActionScript Byte Code) file.
|
|
228
228
|
|