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