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.
Files changed (94) hide show
  1. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/PKG-INFO +56 -22
  2. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/README.md +55 -21
  3. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/__init__.py +5 -3
  4. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/__init__.py +4 -1
  5. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/builder.py +19 -0
  6. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/disasm.py +235 -1
  7. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/types.py +12 -12
  8. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/__init__.py +28 -0
  9. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/call_graph.py +58 -46
  10. pyflashkit-1.2.0/flashkit/analysis/class_graph.py +270 -0
  11. pyflashkit-1.2.0/flashkit/analysis/field_access.py +426 -0
  12. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/inheritance.py +19 -1
  13. pyflashkit-1.2.0/flashkit/analysis/method_fingerprint.py +378 -0
  14. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/references.py +52 -76
  15. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/analysis/strings.py +24 -22
  16. pyflashkit-1.2.0/flashkit/analysis/unified.py +182 -0
  17. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/__init__.py +2 -1
  18. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/callees.py +1 -4
  19. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/callers.py +1 -4
  20. pyflashkit-1.2.0/flashkit/cli/field_access.py +101 -0
  21. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/refs.py +1 -4
  22. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/strings.py +7 -9
  23. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/tree.py +4 -8
  24. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/class_info.py +160 -2
  25. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/member_info.py +109 -4
  26. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/package_info.py +1 -1
  27. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/tags.py +1 -1
  28. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/workspace/resource.py +1 -1
  29. pyflashkit-1.2.0/flashkit/workspace/workspace.py +771 -0
  30. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/PKG-INFO +56 -22
  31. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/SOURCES.txt +12 -5
  32. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyproject.toml +1 -1
  33. pyflashkit-1.2.0/tests/analysis/test_class_graph.py +10 -0
  34. pyflashkit-1.2.0/tests/analysis/test_field_access.py +461 -0
  35. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_inheritance.py +9 -0
  36. pyflashkit-1.2.0/tests/analysis/test_type_hints.py +38 -0
  37. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/cli/test_cli.py +1 -1
  38. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/conftest.py +9 -0
  39. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/info/test_class_info.py +61 -0
  40. pyflashkit-1.2.0/tests/info/test_member_info.py +473 -0
  41. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/test_integration.py +9 -11
  42. pyflashkit-1.2.0/tests/test_public_api.py +41 -0
  43. pyflashkit-1.2.0/tests/workspace/test_workspace.py +414 -0
  44. pyflashkit-1.2.0/tests/workspace/test_workspace_properties.py +44 -0
  45. pyflashkit-1.0.0/flashkit/search/__init__.py +0 -16
  46. pyflashkit-1.0.0/flashkit/search/search.py +0 -456
  47. pyflashkit-1.0.0/flashkit/workspace/workspace.py +0 -232
  48. pyflashkit-1.0.0/tests/search/test_search.py +0 -227
  49. pyflashkit-1.0.0/tests/workspace/__init__.py +0 -0
  50. pyflashkit-1.0.0/tests/workspace/test_workspace.py +0 -206
  51. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/.github/workflows/ci.yml +0 -0
  52. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/.github/workflows/release.yml +0 -0
  53. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/.gitignore +0 -0
  54. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/CONTRIBUTING.md +0 -0
  55. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/LICENSE +0 -0
  56. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/constants.py +0 -0
  57. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/parser.py +0 -0
  58. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/abc/writer.py +0 -0
  59. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/_util.py +0 -0
  60. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/build.py +0 -0
  61. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/class_cmd.py +0 -0
  62. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/classes.py +0 -0
  63. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/disasm.py +0 -0
  64. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/extract.py +0 -0
  65. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/info.py +0 -0
  66. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/packages.py +0 -0
  67. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/cli/tags.py +0 -0
  68. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/errors.py +0 -0
  69. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/info/__init__.py +0 -0
  70. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/__init__.py +0 -0
  71. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/builder.py +0 -0
  72. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/swf/parser.py +0 -0
  73. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/flashkit/workspace/__init__.py +0 -0
  74. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/dependency_links.txt +0 -0
  75. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/entry_points.txt +0 -0
  76. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/requires.txt +0 -0
  77. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/top_level.txt +0 -0
  78. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/setup.cfg +0 -0
  79. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/__init__.py +0 -0
  80. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/__init__.py +0 -0
  81. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_builder.py +0 -0
  82. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_disasm.py +0 -0
  83. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_parser.py +0 -0
  84. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/abc/test_writer.py +0 -0
  85. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/__init__.py +0 -0
  86. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_call_graph.py +0 -0
  87. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_references.py +0 -0
  88. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/analysis/test_strings.py +0 -0
  89. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/cli/__init__.py +0 -0
  90. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/info/__init__.py +0 -0
  91. {pyflashkit-1.0.0/tests/search → pyflashkit-1.2.0/tests/swf}/__init__.py +0 -0
  92. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/swf/test_builder.py +0 -0
  93. {pyflashkit-1.0.0 → pyflashkit-1.2.0}/tests/swf/test_parser.py +0 -0
  94. {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.0.0
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
- from flashkit.analysis import StringIndex
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
- ### 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")
200
+ ### Search and analysis
194
201
 
195
- calls = CallGraph.from_workspace(ws)
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
- strings = StringIndex.from_workspace(ws)
200
- strings.search("config")
201
- strings.url_strings()
202
- strings.classes_using_string("http://example.com")
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
- from flashkit.analysis import StringIndex
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
- ### Analysis
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
- calls = CallGraph.from_workspace(ws)
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
- strings = StringIndex.from_workspace(ws)
179
- strings.search("config")
180
- strings.url_strings()
181
- strings.classes_using_string("http://example.com")
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.0.0"
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