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.
Files changed (48) hide show
  1. flashkit/__init__.py +54 -0
  2. flashkit/abc/__init__.py +79 -0
  3. flashkit/abc/builder.py +847 -0
  4. flashkit/abc/constants.py +198 -0
  5. flashkit/abc/disasm.py +364 -0
  6. flashkit/abc/parser.py +434 -0
  7. flashkit/abc/types.py +275 -0
  8. flashkit/abc/writer.py +230 -0
  9. flashkit/analysis/__init__.py +28 -0
  10. flashkit/analysis/call_graph.py +317 -0
  11. flashkit/analysis/inheritance.py +267 -0
  12. flashkit/analysis/references.py +371 -0
  13. flashkit/analysis/strings.py +299 -0
  14. flashkit/cli/__init__.py +75 -0
  15. flashkit/cli/_util.py +52 -0
  16. flashkit/cli/build.py +36 -0
  17. flashkit/cli/callees.py +30 -0
  18. flashkit/cli/callers.py +30 -0
  19. flashkit/cli/class_cmd.py +83 -0
  20. flashkit/cli/classes.py +71 -0
  21. flashkit/cli/disasm.py +77 -0
  22. flashkit/cli/extract.py +36 -0
  23. flashkit/cli/info.py +41 -0
  24. flashkit/cli/packages.py +30 -0
  25. flashkit/cli/refs.py +31 -0
  26. flashkit/cli/strings.py +58 -0
  27. flashkit/cli/tags.py +32 -0
  28. flashkit/cli/tree.py +52 -0
  29. flashkit/errors.py +33 -0
  30. flashkit/info/__init__.py +31 -0
  31. flashkit/info/class_info.py +176 -0
  32. flashkit/info/member_info.py +275 -0
  33. flashkit/info/package_info.py +60 -0
  34. flashkit/search/__init__.py +16 -0
  35. flashkit/search/search.py +456 -0
  36. flashkit/swf/__init__.py +66 -0
  37. flashkit/swf/builder.py +283 -0
  38. flashkit/swf/parser.py +164 -0
  39. flashkit/swf/tags.py +120 -0
  40. flashkit/workspace/__init__.py +20 -0
  41. flashkit/workspace/resource.py +189 -0
  42. flashkit/workspace/workspace.py +232 -0
  43. pyflashkit-1.0.0.dist-info/METADATA +281 -0
  44. pyflashkit-1.0.0.dist-info/RECORD +48 -0
  45. pyflashkit-1.0.0.dist-info/WHEEL +5 -0
  46. pyflashkit-1.0.0.dist-info/entry_points.txt +2 -0
  47. pyflashkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  48. pyflashkit-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,267 @@
1
+ """
2
+ Inheritance graph for ABC classes.
3
+
4
+ Builds a directed graph of class inheritance and interface implementation
5
+ from resolved ClassInfo data. Supports ancestor/descendant queries,
6
+ interface implementor lookup, and subclass checks.
7
+
8
+ Usage::
9
+
10
+ from flashkit.workspace import Workspace
11
+ from flashkit.analysis.inheritance import InheritanceGraph
12
+
13
+ ws = Workspace()
14
+ ws.load_swf("application.swf")
15
+ graph = InheritanceGraph.from_classes(ws.classes)
16
+
17
+ parent = graph.get_parent("MySprite")
18
+ children = graph.get_children("BaseEntity")
19
+ implementors = graph.get_implementors("IDisposable")
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from collections import defaultdict
26
+
27
+ from ..info.class_info import ClassInfo
28
+
29
+
30
+ @dataclass
31
+ class InheritanceGraph:
32
+ """Directed graph of class inheritance and interface relationships.
33
+
34
+ Attributes:
35
+ classes: Map of qualified name → ClassInfo.
36
+ parent_map: Map of class name → superclass name.
37
+ children_map: Map of class name → set of direct subclass names.
38
+ interface_map: Map of class name → set of interface names it implements.
39
+ implementors_map: Map of interface name → set of class names implementing it.
40
+ """
41
+ classes: dict[str, ClassInfo] = field(default_factory=dict)
42
+ parent_map: dict[str, str] = field(default_factory=dict)
43
+ children_map: dict[str, set[str]] = field(default_factory=lambda: defaultdict(set))
44
+ interface_map: dict[str, set[str]] = field(default_factory=lambda: defaultdict(set))
45
+ implementors_map: dict[str, set[str]] = field(default_factory=lambda: defaultdict(set))
46
+
47
+ @classmethod
48
+ def from_classes(cls, classes: list[ClassInfo]) -> InheritanceGraph:
49
+ """Build an InheritanceGraph from a list of ClassInfo objects.
50
+
51
+ Args:
52
+ classes: All resolved classes (typically from Workspace.classes).
53
+
54
+ Returns:
55
+ Populated InheritanceGraph.
56
+ """
57
+ graph = cls()
58
+
59
+ for ci in classes:
60
+ key = ci.qualified_name
61
+ graph.classes[key] = ci
62
+
63
+ # Parent → child edge
64
+ if ci.super_name and ci.super_name != "*":
65
+ super_qualified = (
66
+ f"{ci.super_package}.{ci.super_name}"
67
+ if ci.super_package else ci.super_name
68
+ )
69
+ graph.parent_map[key] = super_qualified
70
+ graph.children_map[super_qualified].add(key)
71
+
72
+ # Interface edges
73
+ for iface in ci.interfaces:
74
+ graph.interface_map[key].add(iface)
75
+ graph.implementors_map[iface].add(key)
76
+
77
+ return graph
78
+
79
+ def get_parent(self, name: str) -> str | None:
80
+ """Get the direct superclass of a class.
81
+
82
+ Args:
83
+ name: Class name (simple or qualified).
84
+
85
+ Returns:
86
+ Superclass qualified name, or None if not found / root class.
87
+ """
88
+ key = self._resolve_name(name)
89
+ return self.parent_map.get(key) if key else None
90
+
91
+ def get_children(self, name: str) -> list[str]:
92
+ """Get direct subclasses of a class.
93
+
94
+ Args:
95
+ name: Class name (simple or qualified).
96
+
97
+ Returns:
98
+ List of direct subclass qualified names.
99
+ """
100
+ key = self._resolve_name(name)
101
+ if key is None:
102
+ return []
103
+ return sorted(self.children_map.get(key, set()))
104
+
105
+ def get_all_parents(self, name: str) -> list[str]:
106
+ """Get the full ancestor chain (class → superclass → ... → root).
107
+
108
+ Args:
109
+ name: Class name (simple or qualified).
110
+
111
+ Returns:
112
+ List of ancestor qualified names, from immediate parent to root.
113
+ """
114
+ key = self._resolve_name(name)
115
+ if key is None:
116
+ return []
117
+ result: list[str] = []
118
+ visited: set[str] = set()
119
+ current = key
120
+ while current in self.parent_map:
121
+ parent = self.parent_map[current]
122
+ if parent in visited:
123
+ break # circular inheritance guard
124
+ visited.add(parent)
125
+ result.append(parent)
126
+ current = parent
127
+ return result
128
+
129
+ def get_all_children(self, name: str) -> list[str]:
130
+ """Get all descendants (transitive closure of subclasses).
131
+
132
+ Args:
133
+ name: Class name (simple or qualified).
134
+
135
+ Returns:
136
+ List of all descendant qualified names (breadth-first order).
137
+ """
138
+ key = self._resolve_name(name)
139
+ if key is None:
140
+ return []
141
+ result: list[str] = []
142
+ visited: set[str] = set()
143
+ queue = [key]
144
+ while queue:
145
+ current = queue.pop(0)
146
+ for child in sorted(self.children_map.get(current, set())):
147
+ if child not in visited:
148
+ visited.add(child)
149
+ result.append(child)
150
+ queue.append(child)
151
+ return result
152
+
153
+ def get_implementors(self, interface_name: str) -> list[str]:
154
+ """Get all classes that directly implement an interface.
155
+
156
+ Args:
157
+ interface_name: Interface name (simple or qualified).
158
+
159
+ Returns:
160
+ List of implementing class qualified names.
161
+ """
162
+ # Try exact match first, then simple name scan
163
+ if interface_name in self.implementors_map:
164
+ return sorted(self.implementors_map[interface_name])
165
+ # Try matching simple name in the implementors keys
166
+ for key, impls in self.implementors_map.items():
167
+ if key == interface_name or key.endswith(f".{interface_name}"):
168
+ return sorted(impls)
169
+ return []
170
+
171
+ def get_interfaces(self, name: str) -> list[str]:
172
+ """Get all interfaces directly implemented by a class.
173
+
174
+ Args:
175
+ name: Class name (simple or qualified).
176
+
177
+ Returns:
178
+ List of interface name strings.
179
+ """
180
+ key = self._resolve_name(name)
181
+ if key is None:
182
+ return []
183
+ return sorted(self.interface_map.get(key, set()))
184
+
185
+ def get_siblings(self, name: str) -> list[str]:
186
+ """Get classes sharing the same direct parent (excluding self).
187
+
188
+ Args:
189
+ name: Class name (simple or qualified).
190
+
191
+ Returns:
192
+ List of sibling class qualified names.
193
+ """
194
+ key = self._resolve_name(name)
195
+ if key is None:
196
+ return []
197
+ parent = self.parent_map.get(key)
198
+ if parent is None:
199
+ return []
200
+ return sorted(c for c in self.children_map.get(parent, set()) if c != key)
201
+
202
+ def is_subclass(self, child: str, parent: str) -> bool:
203
+ """Check if *child* is a (transitive) subclass of *parent*.
204
+
205
+ Args:
206
+ child: Potential subclass name.
207
+ parent: Potential ancestor name.
208
+
209
+ Returns:
210
+ True if child inherits from parent (directly or transitively).
211
+ """
212
+ child_key = self._resolve_name(child)
213
+ parent_key = self._resolve_name(parent)
214
+ if child_key is None or parent_key is None:
215
+ return False
216
+ ancestors = self.get_all_parents(child_key)
217
+ return parent_key in ancestors
218
+
219
+ def get_roots(self) -> list[str]:
220
+ """Get all root classes (no superclass in the graph).
221
+
222
+ Returns:
223
+ List of root class qualified names.
224
+ """
225
+ roots: list[str] = []
226
+ for name in self.classes:
227
+ parent = self.parent_map.get(name)
228
+ if parent is None or parent not in self.classes:
229
+ roots.append(name)
230
+ return sorted(roots)
231
+
232
+ def get_depth(self, name: str) -> int:
233
+ """Get the inheritance depth of a class (0 for roots).
234
+
235
+ Args:
236
+ name: Class name (simple or qualified).
237
+
238
+ Returns:
239
+ Number of ancestors in the graph, or -1 if not found.
240
+ """
241
+ key = self._resolve_name(name)
242
+ if key is None:
243
+ return -1
244
+ return len(self.get_all_parents(key))
245
+
246
+ def _resolve_name(self, name: str) -> str | None:
247
+ """Resolve a simple or qualified name to a name known in the graph.
248
+
249
+ Checks loaded classes first, then parent/children map keys
250
+ (for external classes like Object that aren't loaded but appear
251
+ as superclasses).
252
+ """
253
+ # Exact match in loaded classes
254
+ if name in self.classes:
255
+ return name
256
+ # Exact match in external references (parent/child keys)
257
+ if name in self.children_map or name in self.parent_map:
258
+ return name
259
+ # Try simple name match in loaded classes
260
+ for qname, ci in self.classes.items():
261
+ if ci.name == name:
262
+ return qname
263
+ # Try simple name match in children_map keys (external classes)
264
+ for key in self.children_map:
265
+ if key.endswith(f".{name}") or key == name:
266
+ return key
267
+ return None
@@ -0,0 +1,371 @@
1
+ """
2
+ Cross-reference index for ABC elements.
3
+
4
+ Builds indexes that answer "where is X used?" questions by scanning
5
+ class traits (field types, method signatures) and method body opcodes.
6
+
7
+ Usage::
8
+
9
+ from flashkit.workspace import Workspace
10
+ from flashkit.analysis.references import ReferenceIndex
11
+
12
+ ws = Workspace()
13
+ ws.load_swf("application.swf")
14
+ refs = ReferenceIndex.from_workspace(ws)
15
+
16
+ users = refs.field_type_users("int")
17
+ creators = refs.instantiators("Point")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass, field
23
+ from collections import defaultdict
24
+
25
+ from ..abc.types import AbcFile
26
+ from ..abc.disasm import decode_instructions
27
+ from ..abc.constants import (
28
+ OP_pushstring, OP_constructprop, OP_callproperty, OP_callpropvoid,
29
+ OP_getlex, OP_coerce, OP_newclass,
30
+ )
31
+ from ..info.member_info import resolve_multiname, build_method_body_map
32
+ from ..info.class_info import ClassInfo
33
+
34
+
35
+ @dataclass
36
+ class Reference:
37
+ """A single cross-reference entry.
38
+
39
+ Attributes:
40
+ source_class: Qualified name of the class containing this reference.
41
+ source_member: Member name (method or field) where this reference occurs.
42
+ target: The referenced name (type name, class name, or string).
43
+ ref_kind: Category: ``"field_type"``, ``"param_type"``, ``"return_type"``,
44
+ ``"instantiation"``, ``"call"``, ``"string_use"``, ``"coerce"``,
45
+ ``"class_ref"``.
46
+ method_index: Method index if this reference is from a method body, else -1.
47
+ offset: Bytecode offset if from a method body, else -1.
48
+ """
49
+ source_class: str
50
+ source_member: str
51
+ target: str
52
+ ref_kind: str
53
+ method_index: int = -1
54
+ offset: int = -1
55
+
56
+
57
+ @dataclass
58
+ class ReferenceIndex:
59
+ """Cross-reference index over all classes and method bodies.
60
+
61
+ Provides efficient lookup for "where is X used?" queries.
62
+
63
+ Attributes:
64
+ refs: All reference entries.
65
+ by_target: Map of target name → list of references to it.
66
+ by_source: Map of source class name → list of references from it.
67
+ """
68
+ refs: list[Reference] = field(default_factory=list)
69
+ by_target: dict[str, list[Reference]] = field(
70
+ default_factory=lambda: defaultdict(list))
71
+ by_source: dict[str, list[Reference]] = field(
72
+ default_factory=lambda: defaultdict(list))
73
+
74
+ def _add(self, ref: Reference) -> None:
75
+ """Add a reference to all indexes."""
76
+ self.refs.append(ref)
77
+ self.by_target[ref.target].append(ref)
78
+ self.by_source[ref.source_class].append(ref)
79
+
80
+ @classmethod
81
+ def from_workspace(cls, workspace: object) -> ReferenceIndex:
82
+ """Build a ReferenceIndex from a Workspace.
83
+
84
+ Scans all class traits and method bodies.
85
+
86
+ Args:
87
+ workspace: A Workspace instance.
88
+
89
+ Returns:
90
+ Populated ReferenceIndex.
91
+ """
92
+ from ..workspace.workspace import Workspace
93
+ ws: Workspace = workspace # type: ignore[assignment]
94
+
95
+ index = cls()
96
+
97
+ for ci in ws.classes:
98
+ index._index_class_traits(ci)
99
+
100
+ for abc in ws.abc_blocks:
101
+ index._index_method_bodies(abc, ws.classes)
102
+
103
+ return index
104
+
105
+ @classmethod
106
+ def from_classes_and_abc(cls, classes: list[ClassInfo],
107
+ abc_blocks: list[AbcFile]) -> ReferenceIndex:
108
+ """Build a ReferenceIndex from class and ABC lists directly.
109
+
110
+ Args:
111
+ classes: All resolved ClassInfo objects.
112
+ abc_blocks: All AbcFile objects.
113
+
114
+ Returns:
115
+ Populated ReferenceIndex.
116
+ """
117
+ index = cls()
118
+ for ci in classes:
119
+ index._index_class_traits(ci)
120
+ for abc in abc_blocks:
121
+ index._index_method_bodies(abc, classes)
122
+ return index
123
+
124
+ def _index_class_traits(self, ci: ClassInfo) -> None:
125
+ """Index field types, method param types, and return types from a class."""
126
+ qname = ci.qualified_name
127
+
128
+ # Field types (instance + static)
129
+ for f in ci.all_fields:
130
+ if f.type_name and f.type_name != "*":
131
+ self._add(Reference(
132
+ source_class=qname,
133
+ source_member=f.name,
134
+ target=f.type_name,
135
+ ref_kind="field_type",
136
+ ))
137
+
138
+ # Method signatures (instance + static)
139
+ for m in ci.all_methods:
140
+ # Return type
141
+ if m.return_type and m.return_type != "*":
142
+ self._add(Reference(
143
+ source_class=qname,
144
+ source_member=m.name,
145
+ target=m.return_type,
146
+ ref_kind="return_type",
147
+ method_index=m.method_index,
148
+ ))
149
+ # Parameter types
150
+ for pt in m.param_types:
151
+ if pt and pt != "*":
152
+ self._add(Reference(
153
+ source_class=qname,
154
+ source_member=m.name,
155
+ target=pt,
156
+ ref_kind="param_type",
157
+ method_index=m.method_index,
158
+ ))
159
+
160
+ # Superclass reference
161
+ if ci.super_name and ci.super_name != "*" and ci.super_name != "Object":
162
+ super_qualified = (
163
+ f"{ci.super_package}.{ci.super_name}"
164
+ if ci.super_package else ci.super_name
165
+ )
166
+ self._add(Reference(
167
+ source_class=qname,
168
+ source_member="<extends>",
169
+ target=super_qualified,
170
+ ref_kind="extends",
171
+ ))
172
+
173
+ # Interface references
174
+ for iface in ci.interfaces:
175
+ self._add(Reference(
176
+ source_class=qname,
177
+ source_member="<implements>",
178
+ target=iface,
179
+ ref_kind="implements",
180
+ ))
181
+
182
+ def _index_method_bodies(self, abc: AbcFile,
183
+ classes: list[ClassInfo]) -> None:
184
+ """Index references from method body opcodes."""
185
+ method_name_map = _build_method_owner_map(abc, classes)
186
+
187
+ for body in abc.method_bodies:
188
+ owner_class = method_name_map.get(body.method, "")
189
+ method_name = f"method_{body.method}"
190
+
191
+ # Find the method name from classes
192
+ for ci in classes:
193
+ for m in ci.all_methods:
194
+ if m.method_index == body.method:
195
+ method_name = m.name
196
+ if not owner_class:
197
+ owner_class = ci.qualified_name
198
+ break
199
+ if ci.constructor_index == body.method:
200
+ method_name = "<init>"
201
+ if not owner_class:
202
+ owner_class = ci.qualified_name
203
+
204
+ try:
205
+ instructions = decode_instructions(body.code)
206
+ except Exception:
207
+ continue
208
+
209
+ for instr in instructions:
210
+ if not instr.operands:
211
+ continue
212
+
213
+ mn_index = instr.operands[0]
214
+
215
+ if instr.opcode == OP_constructprop:
216
+ target = resolve_multiname(abc, mn_index)
217
+ if target != "*" and not target.startswith("multiname["):
218
+ self._add(Reference(
219
+ source_class=owner_class,
220
+ source_member=method_name,
221
+ target=target,
222
+ ref_kind="instantiation",
223
+ method_index=body.method,
224
+ offset=instr.offset,
225
+ ))
226
+
227
+ elif instr.opcode in (OP_callproperty, OP_callpropvoid):
228
+ target = resolve_multiname(abc, mn_index)
229
+ if target != "*" and not target.startswith("multiname["):
230
+ self._add(Reference(
231
+ source_class=owner_class,
232
+ source_member=method_name,
233
+ target=target,
234
+ ref_kind="call",
235
+ method_index=body.method,
236
+ offset=instr.offset,
237
+ ))
238
+
239
+ elif instr.opcode == OP_getlex:
240
+ target = resolve_multiname(abc, mn_index)
241
+ if target != "*" and not target.startswith("multiname["):
242
+ self._add(Reference(
243
+ source_class=owner_class,
244
+ source_member=method_name,
245
+ target=target,
246
+ ref_kind="class_ref",
247
+ method_index=body.method,
248
+ offset=instr.offset,
249
+ ))
250
+
251
+ elif instr.opcode == OP_coerce:
252
+ target = resolve_multiname(abc, mn_index)
253
+ if target != "*" and not target.startswith("multiname["):
254
+ self._add(Reference(
255
+ source_class=owner_class,
256
+ source_member=method_name,
257
+ target=target,
258
+ ref_kind="coerce",
259
+ method_index=body.method,
260
+ offset=instr.offset,
261
+ ))
262
+
263
+ elif instr.opcode == OP_pushstring:
264
+ str_index = instr.operands[0]
265
+ if 0 < str_index < len(abc.string_pool):
266
+ self._add(Reference(
267
+ source_class=owner_class,
268
+ source_member=method_name,
269
+ target=abc.string_pool[str_index],
270
+ ref_kind="string_use",
271
+ method_index=body.method,
272
+ offset=instr.offset,
273
+ ))
274
+
275
+ def field_type_users(self, type_name: str) -> list[Reference]:
276
+ """Find all fields of a given type.
277
+
278
+ Args:
279
+ type_name: The type name to search for.
280
+
281
+ Returns:
282
+ List of references where a field has this type.
283
+ """
284
+ return [r for r in self.by_target.get(type_name, [])
285
+ if r.ref_kind == "field_type"]
286
+
287
+ def method_param_users(self, type_name: str) -> list[Reference]:
288
+ """Find all methods that take a parameter of a given type.
289
+
290
+ Args:
291
+ type_name: The type name to search for.
292
+
293
+ Returns:
294
+ List of references where a method parameter has this type.
295
+ """
296
+ return [r for r in self.by_target.get(type_name, [])
297
+ if r.ref_kind == "param_type"]
298
+
299
+ def method_return_users(self, type_name: str) -> list[Reference]:
300
+ """Find all methods that return a given type.
301
+
302
+ Args:
303
+ type_name: The type name to search for.
304
+
305
+ Returns:
306
+ List of references where a method returns this type.
307
+ """
308
+ return [r for r in self.by_target.get(type_name, [])
309
+ if r.ref_kind == "return_type"]
310
+
311
+ def instantiators(self, class_name: str) -> list[Reference]:
312
+ """Find all places that construct instances of a class.
313
+
314
+ Args:
315
+ class_name: The class being instantiated.
316
+
317
+ Returns:
318
+ List of instantiation references.
319
+ """
320
+ return [r for r in self.by_target.get(class_name, [])
321
+ if r.ref_kind == "instantiation"]
322
+
323
+ def string_users(self, string: str) -> list[Reference]:
324
+ """Find all places that push a specific string constant.
325
+
326
+ Args:
327
+ string: The exact string value.
328
+
329
+ Returns:
330
+ List of string usage references.
331
+ """
332
+ return [r for r in self.by_target.get(string, [])
333
+ if r.ref_kind == "string_use"]
334
+
335
+ def references_from(self, class_name: str) -> list[Reference]:
336
+ """Get all outgoing references from a class.
337
+
338
+ Args:
339
+ class_name: The source class qualified name.
340
+
341
+ Returns:
342
+ All references originating from this class.
343
+ """
344
+ return self.by_source.get(class_name, [])
345
+
346
+ def references_to(self, target: str) -> list[Reference]:
347
+ """Get all incoming references to a target.
348
+
349
+ Args:
350
+ target: The target name (type, class, method, or string).
351
+
352
+ Returns:
353
+ All references pointing to this target.
354
+ """
355
+ return self.by_target.get(target, [])
356
+
357
+ @property
358
+ def total_refs(self) -> int:
359
+ return len(self.refs)
360
+
361
+
362
+ def _build_method_owner_map(abc: AbcFile,
363
+ classes: list[ClassInfo]) -> dict[int, str]:
364
+ """Map method_index → owning class qualified name."""
365
+ owner: dict[int, str] = {}
366
+ for ci in classes:
367
+ owner[ci.constructor_index] = ci.qualified_name
368
+ owner[ci.static_init_index] = ci.qualified_name
369
+ for m in ci.all_methods:
370
+ owner[m.method_index] = ci.qualified_name
371
+ return owner