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,299 @@
1
+ """
2
+ String pool analysis and search.
3
+
4
+ Provides filtered views into the ABC string pool and tracks where each
5
+ string constant is used in method bodies via OP_pushstring instructions.
6
+
7
+ Usage::
8
+
9
+ from flashkit.workspace import Workspace
10
+ from flashkit.analysis.strings import StringIndex
11
+
12
+ ws = Workspace()
13
+ ws.load_swf("application.swf")
14
+ idx = StringIndex.from_workspace(ws)
15
+
16
+ results = idx.search("config")
17
+ classes = idx.classes_using_string("http://")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from dataclasses import dataclass, field
24
+ from collections import defaultdict
25
+
26
+ from ..abc.types import AbcFile
27
+ from ..abc.disasm import decode_instructions
28
+ from ..abc.constants import OP_pushstring, OP_debugfile
29
+ from ..info.member_info import resolve_multiname, build_method_body_map
30
+ from ..info.class_info import ClassInfo
31
+
32
+
33
+ @dataclass
34
+ class StringUsage:
35
+ """A single occurrence of a string constant in bytecode.
36
+
37
+ Attributes:
38
+ string: The string value.
39
+ class_name: Qualified name of the owning class.
40
+ method_name: Method name where the string is pushed.
41
+ method_index: Index into AbcFile.methods.
42
+ offset: Bytecode offset of the OP_pushstring instruction.
43
+ opcode: The opcode (OP_pushstring or OP_debugfile).
44
+ """
45
+ string: str
46
+ class_name: str
47
+ method_name: str
48
+ method_index: int
49
+ offset: int
50
+ opcode: int = OP_pushstring
51
+
52
+
53
+ @dataclass
54
+ class StringIndex:
55
+ """Index of string constant usage across all method bodies.
56
+
57
+ Attributes:
58
+ usages: All string usage entries.
59
+ by_string: Map of string value → list of usages.
60
+ by_class: Map of class name → list of usages.
61
+ all_strings: Set of all unique string values found in code.
62
+ pool_strings: Set of all strings in all string pools (superset).
63
+ """
64
+ usages: list[StringUsage] = field(default_factory=list)
65
+ by_string: dict[str, list[StringUsage]] = field(
66
+ default_factory=lambda: defaultdict(list))
67
+ by_class: dict[str, list[StringUsage]] = field(
68
+ default_factory=lambda: defaultdict(list))
69
+ all_strings: set[str] = field(default_factory=set)
70
+ pool_strings: set[str] = field(default_factory=set)
71
+
72
+ def _add(self, usage: StringUsage) -> None:
73
+ """Add a string usage to all indexes."""
74
+ self.usages.append(usage)
75
+ self.by_string[usage.string].append(usage)
76
+ self.by_class[usage.class_name].append(usage)
77
+ self.all_strings.add(usage.string)
78
+
79
+ @classmethod
80
+ def from_workspace(cls, workspace: object) -> StringIndex:
81
+ """Build a StringIndex from a Workspace.
82
+
83
+ Walks all method bodies, decodes instructions, and collects
84
+ OP_pushstring and OP_debugfile references.
85
+
86
+ Args:
87
+ workspace: A Workspace instance.
88
+
89
+ Returns:
90
+ Populated StringIndex.
91
+ """
92
+ from ..workspace.workspace import Workspace
93
+ ws: Workspace = workspace # type: ignore[assignment]
94
+
95
+ index = cls()
96
+
97
+ # Collect all pool strings
98
+ for abc in ws.abc_blocks:
99
+ for s in abc.string_pool:
100
+ if s:
101
+ index.pool_strings.add(s)
102
+
103
+ for abc in ws.abc_blocks:
104
+ index._index_abc(abc, ws.classes)
105
+
106
+ return index
107
+
108
+ @classmethod
109
+ def from_abc(cls, abc: AbcFile,
110
+ classes: list[ClassInfo] | None = None) -> StringIndex:
111
+ """Build a StringIndex from a single AbcFile.
112
+
113
+ Args:
114
+ abc: The AbcFile to analyze.
115
+ classes: Optional class list for method name resolution.
116
+
117
+ Returns:
118
+ Populated StringIndex.
119
+ """
120
+ index = cls()
121
+ for s in abc.string_pool:
122
+ if s:
123
+ index.pool_strings.add(s)
124
+ index._index_abc(abc, classes or [])
125
+ return index
126
+
127
+ def _index_abc(self, abc: AbcFile, classes: list[ClassInfo]) -> None:
128
+ """Walk all method bodies in an AbcFile and index string usages."""
129
+ method_owner_map = _build_method_owner_map(abc, classes)
130
+ method_name_map = _build_method_name_map(abc, classes)
131
+
132
+ for body in abc.method_bodies:
133
+ owner_class = method_owner_map.get(body.method, "")
134
+ method_name = method_name_map.get(
135
+ body.method, f"method_{body.method}")
136
+
137
+ try:
138
+ instructions = decode_instructions(body.code)
139
+ except Exception:
140
+ continue
141
+
142
+ for instr in instructions:
143
+ if instr.opcode in (OP_pushstring, OP_debugfile):
144
+ if not instr.operands:
145
+ continue
146
+ str_index = instr.operands[0]
147
+ if 0 < str_index < len(abc.string_pool):
148
+ string_val = abc.string_pool[str_index]
149
+ self._add(StringUsage(
150
+ string=string_val,
151
+ class_name=owner_class,
152
+ method_name=method_name,
153
+ method_index=body.method,
154
+ offset=instr.offset,
155
+ opcode=instr.opcode,
156
+ ))
157
+
158
+ def search(self, pattern: str, regex: bool = False) -> list[str]:
159
+ """Search for strings matching a pattern.
160
+
161
+ Args:
162
+ pattern: Substring to search for, or regex if regex=True.
163
+ regex: If True, treat pattern as a regular expression.
164
+
165
+ Returns:
166
+ List of matching string values (from code usage).
167
+ """
168
+ if regex:
169
+ try:
170
+ compiled = re.compile(pattern, re.IGNORECASE)
171
+ except re.error:
172
+ return []
173
+ return sorted(s for s in self.all_strings if compiled.search(s))
174
+ else:
175
+ pattern_lower = pattern.lower()
176
+ return sorted(
177
+ s for s in self.all_strings if pattern_lower in s.lower())
178
+
179
+ def search_pool(self, pattern: str, regex: bool = False) -> list[str]:
180
+ """Search all string pool entries (not just those used in code).
181
+
182
+ Args:
183
+ pattern: Substring or regex pattern.
184
+ regex: If True, treat as regex.
185
+
186
+ Returns:
187
+ List of matching strings from the pool.
188
+ """
189
+ if regex:
190
+ try:
191
+ compiled = re.compile(pattern, re.IGNORECASE)
192
+ except re.error:
193
+ return []
194
+ return sorted(s for s in self.pool_strings if compiled.search(s))
195
+ else:
196
+ pattern_lower = pattern.lower()
197
+ return sorted(
198
+ s for s in self.pool_strings if pattern_lower in s.lower())
199
+
200
+ def strings_in_class(self, class_name: str) -> list[str]:
201
+ """Get all unique strings referenced by a class.
202
+
203
+ Args:
204
+ class_name: Qualified or simple class name.
205
+
206
+ Returns:
207
+ Sorted list of unique string values.
208
+ """
209
+ usages = self.by_class.get(class_name, [])
210
+ if not usages:
211
+ # Try simple name match
212
+ for key, u_list in self.by_class.items():
213
+ if key.endswith(f".{class_name}") or key == class_name:
214
+ usages = u_list
215
+ break
216
+ return sorted(set(u.string for u in usages))
217
+
218
+ def classes_using_string(self, string: str) -> list[str]:
219
+ """Get all classes that reference a specific string.
220
+
221
+ Args:
222
+ string: The exact string value.
223
+
224
+ Returns:
225
+ Sorted list of unique class qualified names.
226
+ """
227
+ return sorted(set(
228
+ u.class_name for u in self.by_string.get(string, [])
229
+ if u.class_name
230
+ ))
231
+
232
+ def debug_markers(self) -> list[str]:
233
+ """Find strings that look like debug source markers (e.g. ``[Foo.hx]``).
234
+
235
+ Returns:
236
+ Sorted list of matching strings.
237
+ """
238
+ return sorted(
239
+ s for s in self.all_strings
240
+ if s.endswith(".hx") or s.endswith(".as")
241
+ or (s.startswith("[") and s.endswith("]"))
242
+ )
243
+
244
+ def url_strings(self) -> list[str]:
245
+ """Find strings that look like URLs or file paths.
246
+
247
+ Returns:
248
+ Sorted list of matching strings.
249
+ """
250
+ return sorted(
251
+ s for s in self.all_strings
252
+ if s.startswith("http://") or s.startswith("https://")
253
+ or s.startswith("file://") or s.startswith("/")
254
+ or ".xml" in s or ".json" in s or ".swf" in s
255
+ )
256
+
257
+ def ui_strings(self) -> list[str]:
258
+ """Find strings that likely represent UI labels (contain spaces, mixed case).
259
+
260
+ Returns:
261
+ Sorted list of matching strings.
262
+ """
263
+ return sorted(
264
+ s for s in self.all_strings
265
+ if " " in s and len(s) > 3 and not s.startswith("http")
266
+ and not s.endswith(".hx") and not s.endswith(".as")
267
+ )
268
+
269
+ @property
270
+ def unique_string_count(self) -> int:
271
+ return len(self.all_strings)
272
+
273
+ @property
274
+ def total_usages(self) -> int:
275
+ return len(self.usages)
276
+
277
+
278
+ def _build_method_owner_map(abc: AbcFile,
279
+ classes: list[ClassInfo]) -> dict[int, str]:
280
+ """Map method_index → owning class qualified name."""
281
+ owner: dict[int, str] = {}
282
+ for ci in classes:
283
+ owner[ci.constructor_index] = ci.qualified_name
284
+ owner[ci.static_init_index] = ci.qualified_name
285
+ for m in ci.all_methods:
286
+ owner[m.method_index] = ci.qualified_name
287
+ return owner
288
+
289
+
290
+ def _build_method_name_map(abc: AbcFile,
291
+ classes: list[ClassInfo]) -> dict[int, str]:
292
+ """Map method_index → readable method name."""
293
+ name_map: dict[int, str] = {}
294
+ for ci in classes:
295
+ name_map[ci.constructor_index] = "<init>"
296
+ name_map[ci.static_init_index] = "<cinit>"
297
+ for m in ci.all_methods:
298
+ name_map[m.method_index] = m.name
299
+ return name_map
@@ -0,0 +1,75 @@
1
+ """
2
+ flashkit CLI — command-line interface for SWF/ABC analysis.
3
+
4
+ Structured as a package with one module per subcommand.
5
+ Entry point is :func:`main`, registered as ``flashkit`` console script.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+
13
+ from .. import __version__
14
+ from ..errors import FlashkitError
15
+ from ._util import red
16
+
17
+
18
+ def build_parser() -> argparse.ArgumentParser:
19
+ """Build the top-level argument parser with all subcommands."""
20
+ parser = argparse.ArgumentParser(
21
+ prog="flashkit",
22
+ description="SWF/ABC toolkit — inspect, analyze, and manipulate Flash files.",
23
+ )
24
+ parser.add_argument(
25
+ "--version", action="version",
26
+ version=f"flashkit {__version__}")
27
+
28
+ sub = parser.add_subparsers(dest="command", metavar="COMMAND")
29
+
30
+ # Import each command module — each one registers itself.
31
+ from . import (
32
+ info, tags, classes, class_cmd, strings,
33
+ disasm, callers, callees, refs, tree,
34
+ packages, extract, build,
35
+ )
36
+
37
+ info.register(sub)
38
+ tags.register(sub)
39
+ classes.register(sub)
40
+ class_cmd.register(sub)
41
+ strings.register(sub)
42
+ disasm.register(sub)
43
+ callers.register(sub)
44
+ callees.register(sub)
45
+ refs.register(sub)
46
+ tree.register(sub)
47
+ packages.register(sub)
48
+ extract.register(sub)
49
+ build.register(sub)
50
+
51
+ return parser
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> None:
55
+ """CLI entry point."""
56
+ parser = build_parser()
57
+ args = parser.parse_args(argv)
58
+
59
+ if not args.command:
60
+ parser.print_help()
61
+ sys.exit(0)
62
+
63
+ try:
64
+ args.func(args)
65
+ except FlashkitError as e:
66
+ print(f"{red('Error')}: {e}", file=sys.stderr)
67
+ sys.exit(1)
68
+ except BrokenPipeError:
69
+ pass
70
+ except KeyboardInterrupt:
71
+ sys.exit(130)
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
flashkit/cli/_util.py ADDED
@@ -0,0 +1,52 @@
1
+ """Shared CLI utilities — ANSI colors and workspace loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+
8
+ from ..workspace import Workspace
9
+
10
+ # Respect NO_COLOR convention (https://no-color.org/) and non-TTY output.
11
+ _NO_COLOR = not sys.stdout.isatty() or os.environ.get("NO_COLOR", "") != ""
12
+
13
+
14
+ def _c(code: str, text: str) -> str:
15
+ if _NO_COLOR:
16
+ return text
17
+ return f"\033[{code}m{text}\033[0m"
18
+
19
+
20
+ def bold(t: str) -> str:
21
+ return _c("1", t)
22
+
23
+
24
+ def dim(t: str) -> str:
25
+ return _c("2", t)
26
+
27
+
28
+ def green(t: str) -> str:
29
+ return _c("32", t)
30
+
31
+
32
+ def cyan(t: str) -> str:
33
+ return _c("36", t)
34
+
35
+
36
+ def yellow(t: str) -> str:
37
+ return _c("33", t)
38
+
39
+
40
+ def red(t: str) -> str:
41
+ return _c("31", t)
42
+
43
+
44
+ def magenta(t: str) -> str:
45
+ return _c("35", t)
46
+
47
+
48
+ def load(path: str) -> Workspace:
49
+ """Load a SWF/SWZ file into a Workspace."""
50
+ ws = Workspace()
51
+ ws.load(path)
52
+ return ws
flashkit/cli/build.py ADDED
@@ -0,0 +1,36 @@
1
+ """``flashkit build`` — rebuild a SWF (recompress or decompress)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from ._util import load
9
+
10
+
11
+ def register(sub: argparse._SubParsersAction) -> None:
12
+ p = sub.add_parser("build", help="Rebuild SWF (recompress/decompress)")
13
+ p.add_argument("file", help="SWF file")
14
+ p.add_argument("-o", "--output", help="Output file path")
15
+ p.add_argument("-d", "--decompress", action="store_true",
16
+ help="Output uncompressed FWS")
17
+ p.set_defaults(func=run)
18
+
19
+
20
+ def run(args: argparse.Namespace) -> None:
21
+ ws = load(args.file)
22
+ res = ws.resources[0]
23
+
24
+ if res.swf_tags is None:
25
+ print("Cannot rebuild: not a SWF file.")
26
+ return
27
+
28
+ from ..swf.builder import rebuild_swf
29
+
30
+ compress = not args.decompress
31
+ output = rebuild_swf(res.swf_header, res.swf_tags, compress=compress)
32
+
33
+ out_path = args.output or args.file
34
+ Path(out_path).write_bytes(output)
35
+ mode = "compressed" if compress else "uncompressed"
36
+ print(f"Wrote {out_path} ({len(output)} bytes, {mode})")
@@ -0,0 +1,30 @@
1
+ """``flashkit callees`` — find calls made from a method."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from ._util import load, bold, dim
8
+
9
+
10
+ def register(sub: argparse._SubParsersAction) -> None:
11
+ p = sub.add_parser("callees", help="Find calls from a method")
12
+ p.add_argument("file", help="SWF or SWZ file")
13
+ p.add_argument("name", help="Method name (e.g. Class.method)")
14
+ p.set_defaults(func=run)
15
+
16
+
17
+ def run(args: argparse.Namespace) -> None:
18
+ ws = load(args.file)
19
+
20
+ from ..analysis.call_graph import CallGraph
21
+ graph = CallGraph.from_workspace(ws)
22
+ edges = graph.get_callees(args.name)
23
+
24
+ if not edges:
25
+ print(f"No callees found for '{args.name}'.")
26
+ return
27
+
28
+ print(bold(f"Callees from '{args.name}'") + f" ({len(edges)} edges)")
29
+ for e in edges:
30
+ print(f" -> {e.target} {dim(e.mnemonic)} @ 0x{e.offset:04X}")
@@ -0,0 +1,30 @@
1
+ """``flashkit callers`` — find callers of a method/property."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from ._util import load, bold, dim
8
+
9
+
10
+ def register(sub: argparse._SubParsersAction) -> None:
11
+ p = sub.add_parser("callers", help="Find callers of a method")
12
+ p.add_argument("file", help="SWF or SWZ file")
13
+ p.add_argument("name", help="Method or property name")
14
+ p.set_defaults(func=run)
15
+
16
+
17
+ def run(args: argparse.Namespace) -> None:
18
+ ws = load(args.file)
19
+
20
+ from ..analysis.call_graph import CallGraph
21
+ graph = CallGraph.from_workspace(ws)
22
+ edges = graph.get_callers(args.name)
23
+
24
+ if not edges:
25
+ print(f"No callers found for '{args.name}'.")
26
+ return
27
+
28
+ print(bold(f"Callers of '{args.name}'") + f" ({len(edges)} edges)")
29
+ for e in edges:
30
+ print(f" {e.caller} {dim(e.mnemonic)} @ 0x{e.offset:04X}")
@@ -0,0 +1,83 @@
1
+ """``flashkit class`` — show details for a single class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from ._util import load, bold, dim, cyan, green, yellow
8
+
9
+
10
+ def register(sub: argparse._SubParsersAction) -> None:
11
+ p = sub.add_parser("class", help="Show class details")
12
+ p.add_argument("file", help="SWF or SWZ file")
13
+ p.add_argument("name", help="Class name (simple or qualified)")
14
+ p.set_defaults(func=run)
15
+
16
+
17
+ def run(args: argparse.Namespace) -> None:
18
+ ws = load(args.file)
19
+ cls = ws.get_class(args.name)
20
+
21
+ if cls is None:
22
+ matches = ws.find_classes(name=args.name)
23
+ if len(matches) == 1:
24
+ cls = matches[0]
25
+ elif matches:
26
+ print(f"Ambiguous name '{args.name}', matches:")
27
+ for m in matches:
28
+ print(f" {m.qualified_name}")
29
+ return
30
+ else:
31
+ print(f"Class '{args.name}' not found.")
32
+ return
33
+
34
+ flags = []
35
+ if cls.is_interface:
36
+ flags.append("interface")
37
+ if cls.is_final:
38
+ flags.append("final")
39
+ if cls.is_sealed:
40
+ flags.append("sealed")
41
+ flag_str = f" [{', '.join(flags)}]" if flags else ""
42
+
43
+ print(bold(cls.qualified_name) + dim(flag_str))
44
+ if cls.package:
45
+ print(f" Package: {cyan(cls.package)}")
46
+ print(f" Extends: {cyan(cls.super_name)}")
47
+ if cls.interfaces:
48
+ print(f" Implements: {', '.join(green(i) for i in cls.interfaces)}")
49
+
50
+ if cls.fields:
51
+ print(f"\n {bold('Instance Fields')} ({len(cls.fields)})")
52
+ for f in cls.fields:
53
+ const = "const " if f.is_const else ""
54
+ print(f" {const}{f.name}: {yellow(f.type_name)}")
55
+
56
+ if cls.static_fields:
57
+ print(f"\n {bold('Static Fields')} ({len(cls.static_fields)})")
58
+ for f in cls.static_fields:
59
+ const = "const " if f.is_const else ""
60
+ print(f" static {const}{f.name}: {yellow(f.type_name)}")
61
+
62
+ if cls.methods:
63
+ print(f"\n {bold('Instance Methods')} ({len(cls.methods)})")
64
+ for m in cls.methods:
65
+ kind = ""
66
+ if m.is_getter:
67
+ kind = "get "
68
+ elif m.is_setter:
69
+ kind = "set "
70
+ params = ", ".join(
71
+ f"{n}: {t}" if n else t
72
+ for n, t in zip(m.param_names or [""] * len(m.param_types),
73
+ m.param_types))
74
+ print(f" {kind}{m.name}({params}): {yellow(m.return_type)}")
75
+
76
+ if cls.static_methods:
77
+ print(f"\n {bold('Static Methods')} ({len(cls.static_methods)})")
78
+ for m in cls.static_methods:
79
+ params = ", ".join(
80
+ f"{n}: {t}" if n else t
81
+ for n, t in zip(m.param_names or [""] * len(m.param_types),
82
+ m.param_types))
83
+ print(f" static {m.name}({params}): {yellow(m.return_type)}")
@@ -0,0 +1,71 @@
1
+ """``flashkit classes`` — list all classes with optional filters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from ._util import load, bold, dim, cyan, green
8
+
9
+
10
+ def register(sub: argparse._SubParsersAction) -> None:
11
+ p = sub.add_parser("classes", help="List classes")
12
+ p.add_argument("file", help="SWF or SWZ file")
13
+ p.add_argument("-s", "--search", help="Filter by name substring")
14
+ p.add_argument("-p", "--package", help="Filter by package")
15
+ p.add_argument("-e", "--extends", help="Filter by superclass")
16
+ p.add_argument("-i", "--interfaces-only", action="store_true",
17
+ help="Show only interfaces")
18
+ p.add_argument("-v", "--verbose", action="store_true",
19
+ help="Show detailed info per class")
20
+ p.set_defaults(func=run)
21
+
22
+
23
+ def run(args: argparse.Namespace) -> None:
24
+ ws = load(args.file)
25
+ classes = ws.classes
26
+
27
+ if args.package:
28
+ classes = [c for c in classes if c.package == args.package]
29
+ if args.extends:
30
+ classes = [c for c in classes if c.super_name == args.extends]
31
+ if args.interfaces_only:
32
+ classes = [c for c in classes if c.is_interface]
33
+ if args.search:
34
+ term = args.search.lower()
35
+ classes = [c for c in classes if term in c.qualified_name.lower()]
36
+
37
+ if not classes:
38
+ print("No classes found.")
39
+ return
40
+
41
+ if args.verbose:
42
+ for cls in classes:
43
+ flags = []
44
+ if cls.is_interface:
45
+ flags.append("interface")
46
+ if cls.is_final:
47
+ flags.append("final")
48
+ if cls.is_sealed:
49
+ flags.append("sealed")
50
+ flag_str = f" [{', '.join(flags)}]" if flags else ""
51
+
52
+ print(bold(cls.qualified_name) + dim(flag_str))
53
+ print(f" extends {cyan(cls.super_name)}")
54
+ if cls.interfaces:
55
+ print(f" implements {', '.join(green(i) for i in cls.interfaces)}")
56
+ print(f" {len(cls.fields)} fields, {len(cls.methods)} methods"
57
+ f", {len(cls.static_fields)} static fields"
58
+ f", {len(cls.static_methods)} static methods")
59
+ print()
60
+ else:
61
+ print(bold(f"{'Class':<50} {'Super':<25} {'Fields':>6} {'Methods':>7}"))
62
+ print("-" * 92)
63
+ for cls in classes:
64
+ name = cls.qualified_name
65
+ prefix = ""
66
+ if cls.is_interface:
67
+ prefix = dim("[I] ")
68
+ print(f"{prefix}{name:<50} {cls.super_name:<25} "
69
+ f"{len(cls.all_fields):>6} {len(cls.all_methods):>7}")
70
+
71
+ print(f"\n{len(classes)} class(es)")