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,456 @@
1
+ """
2
+ Unified search engine for workspace content.
3
+
4
+ Provides a high-level query API that combines all analysis indexes
5
+ (inheritance, call graph, references, strings) into a single interface.
6
+
7
+ Usage::
8
+
9
+ from flashkit.workspace import Workspace
10
+ from flashkit.search import SearchEngine
11
+
12
+ ws = Workspace()
13
+ ws.load_swf("application.swf")
14
+ engine = SearchEngine(ws)
15
+
16
+ # Find classes extending a base
17
+ subclasses = engine.find_subclasses("BaseSprite")
18
+
19
+ # Find who instantiates a class
20
+ creators = engine.find_instantiators("Point")
21
+
22
+ # Find classes using a specific string
23
+ matches = engine.find_by_string("config.xml")
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass, field
29
+
30
+ from ..info.class_info import ClassInfo
31
+ from ..analysis.inheritance import InheritanceGraph
32
+ from ..analysis.call_graph import CallGraph, CallEdge
33
+ from ..analysis.references import ReferenceIndex, Reference
34
+ from ..analysis.strings import StringIndex, StringUsage
35
+
36
+
37
+ @dataclass
38
+ class ClassResult:
39
+ """A class search result with context about why it matched.
40
+
41
+ Attributes:
42
+ class_info: The matched ClassInfo.
43
+ match_reason: Why this class matched (e.g. ``"extends BaseSprite"``).
44
+ """
45
+ class_info: ClassInfo
46
+ match_reason: str = ""
47
+
48
+ @property
49
+ def name(self) -> str:
50
+ return self.class_info.qualified_name
51
+
52
+
53
+ @dataclass
54
+ class MemberResult:
55
+ """A member (field or method) search result.
56
+
57
+ Attributes:
58
+ class_name: Owning class qualified name.
59
+ member_name: Field or method name.
60
+ member_type: ``"field"`` or ``"method"``.
61
+ match_reason: Why this member matched.
62
+ """
63
+ class_name: str
64
+ member_name: str
65
+ member_type: str
66
+ match_reason: str = ""
67
+
68
+
69
+ @dataclass
70
+ class StringResult:
71
+ """A string search result.
72
+
73
+ Attributes:
74
+ string: The matched string value.
75
+ usages: Where this string is used.
76
+ """
77
+ string: str
78
+ usages: list[StringUsage] = field(default_factory=list)
79
+
80
+
81
+ class SearchEngine:
82
+ """Unified query interface over all analysis indexes.
83
+
84
+ Lazily builds analysis indexes on first use.
85
+
86
+ Args:
87
+ workspace: A Workspace instance with loaded content.
88
+ """
89
+
90
+ def __init__(self, workspace: object) -> None:
91
+ from ..workspace.workspace import Workspace
92
+ self._ws: Workspace = workspace # type: ignore[assignment]
93
+ self._inheritance: InheritanceGraph | None = None
94
+ self._call_graph: CallGraph | None = None
95
+ self._references: ReferenceIndex | None = None
96
+ self._strings: StringIndex | None = None
97
+
98
+ @property
99
+ def inheritance(self) -> InheritanceGraph:
100
+ """Lazily built inheritance graph."""
101
+ if self._inheritance is None:
102
+ self._inheritance = InheritanceGraph.from_classes(self._ws.classes)
103
+ return self._inheritance
104
+
105
+ @property
106
+ def call_graph(self) -> CallGraph:
107
+ """Lazily built call graph."""
108
+ if self._call_graph is None:
109
+ self._call_graph = CallGraph.from_workspace(self._ws)
110
+ return self._call_graph
111
+
112
+ @property
113
+ def references(self) -> ReferenceIndex:
114
+ """Lazily built reference index."""
115
+ if self._references is None:
116
+ self._references = ReferenceIndex.from_workspace(self._ws)
117
+ return self._references
118
+
119
+ @property
120
+ def strings(self) -> StringIndex:
121
+ """Lazily built string index."""
122
+ if self._strings is None:
123
+ self._strings = StringIndex.from_workspace(self._ws)
124
+ return self._strings
125
+
126
+ # ── Class queries ──────────────────────────────────────────────────────
127
+
128
+ def find_classes(
129
+ self,
130
+ *,
131
+ name: str | None = None,
132
+ extends: str | None = None,
133
+ implements: str | None = None,
134
+ package: str | None = None,
135
+ is_interface: bool | None = None,
136
+ ) -> list[ClassResult]:
137
+ """Find classes matching criteria (delegates to Workspace.find_classes).
138
+
139
+ All criteria are AND-combined.
140
+
141
+ Args:
142
+ name: Substring match on class name.
143
+ extends: Exact match on superclass name.
144
+ implements: Exact match on one of the interface names.
145
+ package: Exact match on package name.
146
+ is_interface: Filter by interface flag.
147
+
148
+ Returns:
149
+ List of ClassResult objects.
150
+ """
151
+ matches = self._ws.find_classes(
152
+ name=name, extends=extends, implements=implements,
153
+ package=package, is_interface=is_interface,
154
+ )
155
+ reasons: list[str] = []
156
+ if name:
157
+ reasons.append(f"name contains '{name}'")
158
+ if extends:
159
+ reasons.append(f"extends {extends}")
160
+ if implements:
161
+ reasons.append(f"implements {implements}")
162
+ if package:
163
+ reasons.append(f"in package {package}")
164
+ if is_interface is not None:
165
+ reasons.append("is interface" if is_interface else "is class")
166
+ reason = ", ".join(reasons)
167
+ return [ClassResult(class_info=c, match_reason=reason) for c in matches]
168
+
169
+ def find_subclasses(self, class_name: str,
170
+ transitive: bool = False) -> list[ClassResult]:
171
+ """Find direct or transitive subclasses.
172
+
173
+ Args:
174
+ class_name: The parent class name.
175
+ transitive: If True, include all descendants.
176
+
177
+ Returns:
178
+ List of ClassResult objects.
179
+ """
180
+ if transitive:
181
+ names = self.inheritance.get_all_children(class_name)
182
+ else:
183
+ names = self.inheritance.get_children(class_name)
184
+
185
+ results: list[ClassResult] = []
186
+ for n in names:
187
+ ci = self._ws.get_class(n)
188
+ if ci:
189
+ results.append(ClassResult(
190
+ class_info=ci,
191
+ match_reason=f"{'transitively ' if transitive else ''}extends {class_name}",
192
+ ))
193
+ return results
194
+
195
+ def find_implementors(self, interface_name: str) -> list[ClassResult]:
196
+ """Find classes implementing an interface.
197
+
198
+ Args:
199
+ interface_name: The interface name.
200
+
201
+ Returns:
202
+ List of ClassResult objects.
203
+ """
204
+ names = self.inheritance.get_implementors(interface_name)
205
+ results: list[ClassResult] = []
206
+ for n in names:
207
+ ci = self._ws.get_class(n)
208
+ if ci:
209
+ results.append(ClassResult(
210
+ class_info=ci,
211
+ match_reason=f"implements {interface_name}",
212
+ ))
213
+ return results
214
+
215
+ # ── Member queries ─────────────────────────────────────────────────────
216
+
217
+ def find_fields(
218
+ self,
219
+ *,
220
+ name: str | None = None,
221
+ type_name: str | None = None,
222
+ is_static: bool | None = None,
223
+ ) -> list[MemberResult]:
224
+ """Find fields across all classes.
225
+
226
+ Args:
227
+ name: Substring match on field name.
228
+ type_name: Exact match on field type.
229
+ is_static: Filter by static flag.
230
+
231
+ Returns:
232
+ List of MemberResult objects.
233
+ """
234
+ results: list[MemberResult] = []
235
+ for ci in self._ws.classes:
236
+ for f in ci.all_fields:
237
+ if name is not None and name not in f.name:
238
+ continue
239
+ if type_name is not None and f.type_name != type_name:
240
+ continue
241
+ if is_static is not None and f.is_static != is_static:
242
+ continue
243
+ reason_parts = []
244
+ if name:
245
+ reason_parts.append(f"name contains '{name}'")
246
+ if type_name:
247
+ reason_parts.append(f"type={type_name}")
248
+ results.append(MemberResult(
249
+ class_name=ci.qualified_name,
250
+ member_name=f.name,
251
+ member_type="field",
252
+ match_reason=", ".join(reason_parts),
253
+ ))
254
+ return results
255
+
256
+ def find_methods(
257
+ self,
258
+ *,
259
+ name: str | None = None,
260
+ return_type: str | None = None,
261
+ param_type: str | None = None,
262
+ is_static: bool | None = None,
263
+ ) -> list[MemberResult]:
264
+ """Find methods across all classes.
265
+
266
+ Args:
267
+ name: Substring match on method name.
268
+ return_type: Exact match on return type.
269
+ param_type: Exact match on any parameter type.
270
+ is_static: Filter by static flag.
271
+
272
+ Returns:
273
+ List of MemberResult objects.
274
+ """
275
+ results: list[MemberResult] = []
276
+ for ci in self._ws.classes:
277
+ for m in ci.all_methods:
278
+ if name is not None and name not in m.name:
279
+ continue
280
+ if return_type is not None and m.return_type != return_type:
281
+ continue
282
+ if param_type is not None and param_type not in m.param_types:
283
+ continue
284
+ if is_static is not None and m.is_static != is_static:
285
+ continue
286
+ reason_parts = []
287
+ if name:
288
+ reason_parts.append(f"name contains '{name}'")
289
+ if return_type:
290
+ reason_parts.append(f"returns {return_type}")
291
+ if param_type:
292
+ reason_parts.append(f"takes {param_type}")
293
+ results.append(MemberResult(
294
+ class_name=ci.qualified_name,
295
+ member_name=m.name,
296
+ member_type="method",
297
+ match_reason=", ".join(reason_parts),
298
+ ))
299
+ return results
300
+
301
+ # ── Reference queries ──────────────────────────────────────────────────
302
+
303
+ def find_instantiators(self, class_name: str) -> list[Reference]:
304
+ """Find all places that construct instances of a class.
305
+
306
+ Args:
307
+ class_name: The class being instantiated.
308
+
309
+ Returns:
310
+ List of Reference objects with ref_kind="instantiation".
311
+ """
312
+ return self.references.instantiators(class_name)
313
+
314
+ def find_type_users(self, type_name: str) -> list[Reference]:
315
+ """Find all places that reference a type (fields, params, returns).
316
+
317
+ Args:
318
+ type_name: The type name.
319
+
320
+ Returns:
321
+ Combined list of field_type, param_type, and return_type references.
322
+ """
323
+ result = self.references.field_type_users(type_name)
324
+ result += self.references.method_param_users(type_name)
325
+ result += self.references.method_return_users(type_name)
326
+ return result
327
+
328
+ def find_callers(self, method_name: str) -> list[CallEdge]:
329
+ """Find all callers of a method.
330
+
331
+ Args:
332
+ method_name: The method name.
333
+
334
+ Returns:
335
+ List of CallEdge objects.
336
+ """
337
+ return self.call_graph.get_callers(method_name)
338
+
339
+ def find_callees(self, caller: str) -> list[CallEdge]:
340
+ """Find all methods/properties called by a method.
341
+
342
+ Args:
343
+ caller: The caller method name (``"Class.method"``).
344
+
345
+ Returns:
346
+ List of CallEdge objects.
347
+ """
348
+ return self.call_graph.get_callees(caller)
349
+
350
+ # ── String queries ─────────────────────────────────────────────────────
351
+
352
+ def find_by_string(self, pattern: str,
353
+ regex: bool = False) -> list[StringResult]:
354
+ """Find string constants matching a pattern.
355
+
356
+ Args:
357
+ pattern: Substring or regex pattern.
358
+ regex: If True, treat as regex.
359
+
360
+ Returns:
361
+ List of StringResult objects with usage locations.
362
+ """
363
+ matching = self.strings.search(pattern, regex=regex)
364
+ results: list[StringResult] = []
365
+ for s in matching:
366
+ results.append(StringResult(
367
+ string=s,
368
+ usages=self.strings.by_string.get(s, []),
369
+ ))
370
+ return results
371
+
372
+ def find_classes_by_string(self, string: str) -> list[ClassResult]:
373
+ """Find classes that reference a specific string constant.
374
+
375
+ Args:
376
+ string: The exact string value.
377
+
378
+ Returns:
379
+ List of ClassResult objects.
380
+ """
381
+ class_names = self.strings.classes_using_string(string)
382
+ results: list[ClassResult] = []
383
+ for n in class_names:
384
+ ci = self._ws.get_class(n)
385
+ if ci:
386
+ results.append(ClassResult(
387
+ class_info=ci,
388
+ match_reason=f"uses string '{string[:50]}'",
389
+ ))
390
+ return results
391
+
392
+ # ── Structural pattern queries ─────────────────────────────────────────
393
+
394
+ def find_classes_with_field_type(self, type_name: str) -> list[ClassResult]:
395
+ """Find classes that have a field of the given type.
396
+
397
+ Args:
398
+ type_name: The field type name.
399
+
400
+ Returns:
401
+ List of ClassResult objects.
402
+ """
403
+ seen: set[str] = set()
404
+ results: list[ClassResult] = []
405
+ for ci in self._ws.classes:
406
+ for f in ci.all_fields:
407
+ if f.type_name == type_name and ci.qualified_name not in seen:
408
+ seen.add(ci.qualified_name)
409
+ results.append(ClassResult(
410
+ class_info=ci,
411
+ match_reason=f"has field of type {type_name}",
412
+ ))
413
+ return results
414
+
415
+ def find_classes_with_method_returning(
416
+ self, return_type: str,
417
+ ) -> list[ClassResult]:
418
+ """Find classes that have a method returning the given type.
419
+
420
+ Args:
421
+ return_type: The return type name.
422
+
423
+ Returns:
424
+ List of ClassResult objects.
425
+ """
426
+ seen: set[str] = set()
427
+ results: list[ClassResult] = []
428
+ for ci in self._ws.classes:
429
+ for m in ci.all_methods:
430
+ if m.return_type == return_type and ci.qualified_name not in seen:
431
+ seen.add(ci.qualified_name)
432
+ results.append(ClassResult(
433
+ class_info=ci,
434
+ match_reason=f"has method returning {return_type}",
435
+ ))
436
+ return results
437
+
438
+ # ── Summary ────────────────────────────────────────────────────────────
439
+
440
+ def summary(self) -> str:
441
+ """Return a summary of the search engine's indexed data."""
442
+ lines = [f"SearchEngine over {self._ws.class_count} classes"]
443
+ if self._inheritance:
444
+ lines.append(
445
+ f" Inheritance: {len(self._inheritance.classes)} nodes")
446
+ if self._call_graph:
447
+ lines.append(
448
+ f" Call graph: {self._call_graph.edge_count} edges")
449
+ if self._references:
450
+ lines.append(
451
+ f" References: {self._references.total_refs} refs")
452
+ if self._strings:
453
+ lines.append(
454
+ f" Strings: {self._strings.unique_string_count} unique, "
455
+ f"{self._strings.total_usages} usages")
456
+ return "\n".join(lines)
@@ -0,0 +1,66 @@
1
+ """
2
+ SWF container format handling.
3
+
4
+ This package handles the SWF (Small Web Format) container — the file
5
+ format used by Adobe Flash Player. A SWF file is a sequence of typed
6
+ tags containing graphics, sounds, scripts (ABC bytecode), and metadata.
7
+
8
+ Quick start::
9
+
10
+ from flashkit.swf import parse_swf, rebuild_swf, TAG_DO_ABC2
11
+
12
+ header, tags, version, length = parse_swf(swf_bytes)
13
+ output = rebuild_swf(header, tags, compress=True)
14
+ """
15
+
16
+ from .tags import (
17
+ SWFTag,
18
+ TAG_NAMES,
19
+ TAG_END,
20
+ TAG_SHOW_FRAME,
21
+ TAG_SET_BACKGROUND_COLOR,
22
+ TAG_SCRIPT_LIMITS,
23
+ TAG_FILE_ATTRIBUTES,
24
+ TAG_DO_ABC,
25
+ TAG_SYMBOL_CLASS,
26
+ TAG_DEFINE_BINARY_DATA,
27
+ TAG_DO_ABC2,
28
+ TAG_DEFINE_SCENE_AND_FRAME_LABEL,
29
+ TAG_DEBUG_ID,
30
+ )
31
+ from .parser import parse_swf, print_tags
32
+ from .builder import (
33
+ build_tag_bytes,
34
+ rebuild_swf,
35
+ make_doabc2_tag,
36
+ make_symbol_class_tag,
37
+ make_end_tag,
38
+ SwfBuilder,
39
+ )
40
+
41
+ __all__ = [
42
+ # Tags
43
+ "SWFTag",
44
+ "TAG_NAMES",
45
+ "TAG_END",
46
+ "TAG_SHOW_FRAME",
47
+ "TAG_SET_BACKGROUND_COLOR",
48
+ "TAG_SCRIPT_LIMITS",
49
+ "TAG_FILE_ATTRIBUTES",
50
+ "TAG_DO_ABC",
51
+ "TAG_SYMBOL_CLASS",
52
+ "TAG_DEFINE_BINARY_DATA",
53
+ "TAG_DO_ABC2",
54
+ "TAG_DEFINE_SCENE_AND_FRAME_LABEL",
55
+ "TAG_DEBUG_ID",
56
+ # Parser
57
+ "parse_swf",
58
+ "print_tags",
59
+ # Builder
60
+ "build_tag_bytes",
61
+ "rebuild_swf",
62
+ "make_doabc2_tag",
63
+ "make_symbol_class_tag",
64
+ "make_end_tag",
65
+ "SwfBuilder",
66
+ ]