pyflashkit 1.1.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.
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/PKG-INFO +1 -1
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/__init__.py +5 -1
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/abc/__init__.py +3 -1
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/abc/disasm.py +124 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/analysis/__init__.py +22 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/analysis/call_graph.py +5 -1
- pyflashkit-1.2.0/flashkit/analysis/class_graph.py +270 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/analysis/field_access.py +6 -3
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/analysis/inheritance.py +18 -0
- pyflashkit-1.2.0/flashkit/analysis/method_fingerprint.py +378 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/analysis/references.py +6 -3
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/analysis/strings.py +6 -3
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/analysis/unified.py +7 -3
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/info/class_info.py +73 -4
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/info/member_info.py +32 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/workspace/workspace.py +82 -5
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/PKG-INFO +1 -1
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/SOURCES.txt +7 -1
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/pyproject.toml +1 -1
- pyflashkit-1.2.0/tests/analysis/test_class_graph.py +10 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/analysis/test_inheritance.py +9 -0
- pyflashkit-1.2.0/tests/analysis/test_type_hints.py +38 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/cli/test_cli.py +1 -1
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/conftest.py +9 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/info/test_class_info.py +61 -0
- pyflashkit-1.2.0/tests/test_public_api.py +41 -0
- pyflashkit-1.2.0/tests/workspace/test_workspace_properties.py +44 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/.github/workflows/ci.yml +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/.github/workflows/release.yml +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/.gitignore +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/CONTRIBUTING.md +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/LICENSE +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/README.md +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/abc/builder.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/abc/constants.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/abc/parser.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/abc/types.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/abc/writer.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/_util.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/build.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/callees.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/callers.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/class_cmd.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/classes.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/disasm.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/extract.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/field_access.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/info.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/packages.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/refs.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/strings.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/tags.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/cli/tree.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/errors.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/info/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/info/package_info.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/swf/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/swf/builder.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/swf/parser.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/swf/tags.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/workspace/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/flashkit/workspace/resource.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/dependency_links.txt +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/entry_points.txt +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/requires.txt +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/pyflashkit.egg-info/top_level.txt +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/setup.cfg +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/abc/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/abc/test_builder.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/abc/test_disasm.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/abc/test_parser.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/abc/test_writer.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/analysis/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/analysis/test_call_graph.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/analysis/test_field_access.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/analysis/test_references.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/analysis/test_strings.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/cli/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/info/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/info/test_member_info.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/swf/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/swf/test_builder.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/swf/test_parser.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/test_integration.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/workspace/__init__.py +0 -0
- {pyflashkit-1.1.0 → pyflashkit-1.2.0}/tests/workspace/test_workspace.py +0 -0
|
@@ -21,7 +21,7 @@ Quick start::
|
|
|
21
21
|
output = serialize_abc(abc)
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
__version__ = "1.
|
|
24
|
+
__version__ = "1.2.0"
|
|
25
25
|
|
|
26
26
|
from .errors import (
|
|
27
27
|
FlashkitError, ParseError, SWFParseError,
|
|
@@ -32,6 +32,8 @@ from .swf.builder import rebuild_swf, make_doabc2_tag
|
|
|
32
32
|
from .abc.parser import parse_abc
|
|
33
33
|
from .abc.writer import serialize_abc
|
|
34
34
|
from .abc.types import AbcFile
|
|
35
|
+
from .workspace.workspace import Workspace
|
|
36
|
+
from .info.class_info import ClassInfo
|
|
35
37
|
|
|
36
38
|
__all__ = [
|
|
37
39
|
"__version__",
|
|
@@ -49,4 +51,6 @@ __all__ = [
|
|
|
49
51
|
"parse_abc",
|
|
50
52
|
"serialize_abc",
|
|
51
53
|
"AbcFile",
|
|
54
|
+
"Workspace",
|
|
55
|
+
"ClassInfo",
|
|
52
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, scan_relevant_opcodes
|
|
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,9 @@ __all__ = [
|
|
|
73
73
|
"serialize_abc",
|
|
74
74
|
# Disassembler
|
|
75
75
|
"Instruction",
|
|
76
|
+
"ResolvedInstruction",
|
|
76
77
|
"decode_instructions",
|
|
78
|
+
"resolve_instructions",
|
|
77
79
|
"scan_relevant_opcodes",
|
|
78
80
|
# Builder
|
|
79
81
|
"AbcBuilder",
|
|
@@ -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:
|
|
@@ -472,3 +490,109 @@ def decode_instructions(code: bytes,
|
|
|
472
490
|
operands=operands, size=off - start))
|
|
473
491
|
|
|
474
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
|
|
@@ -11,6 +11,8 @@ Modules:
|
|
|
11
11
|
references: ReferenceIndex — cross-references (field types, instantiations, imports).
|
|
12
12
|
strings: StringIndex — string constant search and classification.
|
|
13
13
|
field_access: FieldAccessIndex — field read/write tracking from bytecode.
|
|
14
|
+
method_fingerprint: MethodFingerprint — structural features of method bodies.
|
|
15
|
+
class_graph: ClassGraph — class-to-class reference graph with typed edges.
|
|
14
16
|
"""
|
|
15
17
|
|
|
16
18
|
from .inheritance import InheritanceGraph
|
|
@@ -18,6 +20,18 @@ from .call_graph import CallGraph, CallEdge
|
|
|
18
20
|
from .references import ReferenceIndex, Reference
|
|
19
21
|
from .strings import StringIndex, StringUsage
|
|
20
22
|
from .field_access import FieldAccessIndex, FieldAccess
|
|
23
|
+
from .method_fingerprint import (
|
|
24
|
+
MethodFingerprint,
|
|
25
|
+
extract_fingerprint,
|
|
26
|
+
extract_constructor_fingerprint,
|
|
27
|
+
extract_all_fingerprints,
|
|
28
|
+
)
|
|
29
|
+
from .class_graph import (
|
|
30
|
+
ClassGraph,
|
|
31
|
+
ClassNode,
|
|
32
|
+
FRAMEWORK_TYPES,
|
|
33
|
+
CLASS_EDGE_KINDS,
|
|
34
|
+
)
|
|
21
35
|
from .unified import build_all_indexes
|
|
22
36
|
|
|
23
37
|
__all__ = [
|
|
@@ -30,5 +44,13 @@ __all__ = [
|
|
|
30
44
|
"StringUsage",
|
|
31
45
|
"FieldAccessIndex",
|
|
32
46
|
"FieldAccess",
|
|
47
|
+
"MethodFingerprint",
|
|
48
|
+
"extract_fingerprint",
|
|
49
|
+
"extract_constructor_fingerprint",
|
|
50
|
+
"extract_all_fingerprints",
|
|
51
|
+
"ClassGraph",
|
|
52
|
+
"ClassNode",
|
|
53
|
+
"FRAMEWORK_TYPES",
|
|
54
|
+
"CLASS_EDGE_KINDS",
|
|
33
55
|
"build_all_indexes",
|
|
34
56
|
]
|
|
@@ -22,6 +22,10 @@ from __future__ import annotations
|
|
|
22
22
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from collections import defaultdict
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from ..workspace.workspace import Workspace
|
|
25
29
|
|
|
26
30
|
from ..abc.types import AbcFile
|
|
27
31
|
from ..abc.disasm import scan_relevant_opcodes
|
|
@@ -115,7 +119,7 @@ class CallGraph:
|
|
|
115
119
|
default_factory=lambda: defaultdict(list))
|
|
116
120
|
|
|
117
121
|
@classmethod
|
|
118
|
-
def from_workspace(cls, workspace:
|
|
122
|
+
def from_workspace(cls, workspace: Workspace) -> CallGraph:
|
|
119
123
|
"""Build a CallGraph from a Workspace.
|
|
120
124
|
|
|
121
125
|
Iterates all ABC blocks and their method bodies, decodes
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Class-reference graph built from a Workspace.
|
|
2
|
+
|
|
3
|
+
Wraps ``ReferenceIndex`` into a per-class adjacency structure with typed
|
|
4
|
+
edges and per-node intrinsic features. Each user-defined class becomes a
|
|
5
|
+
node; each cross-reference between two user-defined classes becomes a
|
|
6
|
+
directed, typed edge.
|
|
7
|
+
|
|
8
|
+
Edges to framework/builtin types (Sprite, Event, Array, etc.) are
|
|
9
|
+
filtered out — they add noise without structural information. Strings
|
|
10
|
+
are collected per class in ``string_pool`` and indexed globally in
|
|
11
|
+
``ClassGraph.string_to_classes`` for quick reverse lookups.
|
|
12
|
+
|
|
13
|
+
Each node also carries method fingerprints
|
|
14
|
+
(see :mod:`flashkit.analysis.method_fingerprint`) so downstream code can
|
|
15
|
+
reason about method shapes without re-walking the ABC.
|
|
16
|
+
|
|
17
|
+
Typical usage::
|
|
18
|
+
|
|
19
|
+
from flashkit.workspace import Workspace
|
|
20
|
+
from flashkit.analysis import ClassGraph
|
|
21
|
+
|
|
22
|
+
ws = Workspace()
|
|
23
|
+
ws.load_swf("game.swf")
|
|
24
|
+
g = ClassGraph.from_workspace(ws)
|
|
25
|
+
|
|
26
|
+
node = g.nodes["PlayerController"]
|
|
27
|
+
print(node.out_degree_by_kind) # {'call': 12, 'field_type': 3, ...}
|
|
28
|
+
print(len(node.method_fps), "methods fingerprinted")
|
|
29
|
+
print(g.string_to_classes["Hello"]) # which classes reference "Hello"
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from collections import defaultdict
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import TYPE_CHECKING
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from ..workspace.workspace import Workspace
|
|
40
|
+
|
|
41
|
+
from .references import ReferenceIndex
|
|
42
|
+
from .method_fingerprint import (
|
|
43
|
+
BUILTIN_TYPES,
|
|
44
|
+
MethodFingerprint,
|
|
45
|
+
extract_all_fingerprints,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"FRAMEWORK_TYPES",
|
|
51
|
+
"CLASS_EDGE_KINDS",
|
|
52
|
+
"ClassNode",
|
|
53
|
+
"ClassGraph",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Framework types filtered out of class-to-class edges. These are Flash
|
|
58
|
+
# Player runtime / AS3 builtins; references to them aren't meaningful
|
|
59
|
+
# when comparing user-defined class structure.
|
|
60
|
+
FRAMEWORK_TYPES: frozenset[str] = frozenset({
|
|
61
|
+
"int", "uint", "Number", "String", "Boolean", "void", "Object",
|
|
62
|
+
"Array", "Class", "Function", "*", "Namespace", "QName",
|
|
63
|
+
"ByteArray", "Dictionary", "Date", "RegExp", "Error", "XML",
|
|
64
|
+
"XMLList",
|
|
65
|
+
# Display list
|
|
66
|
+
"Sprite", "MovieClip", "DisplayObject", "DisplayObjectContainer",
|
|
67
|
+
"BitmapData", "Bitmap", "Shape", "TextField", "TextFormat",
|
|
68
|
+
# Events
|
|
69
|
+
"Event", "EventDispatcher", "MouseEvent", "KeyboardEvent",
|
|
70
|
+
"TimerEvent", "IOErrorEvent", "SecurityErrorEvent", "ProgressEvent",
|
|
71
|
+
# Geometry
|
|
72
|
+
"Point", "Rectangle", "Matrix", "ColorTransform",
|
|
73
|
+
# Audio
|
|
74
|
+
"Sound", "SoundChannel", "SoundTransform",
|
|
75
|
+
# Network / IO
|
|
76
|
+
"URLRequest", "URLLoader", "SharedObject", "Socket",
|
|
77
|
+
# Misc
|
|
78
|
+
"Timer", "Stage", "Loader", "LoaderInfo", "NetConnection",
|
|
79
|
+
"Vector", "BlendMode",
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
# Edge kinds that represent class-to-class relationships.
|
|
83
|
+
CLASS_EDGE_KINDS: frozenset[str] = frozenset({
|
|
84
|
+
"extends", "implements",
|
|
85
|
+
"field_type", "param_type", "return_type",
|
|
86
|
+
"call", "instantiation", "class_ref", "coerce",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _normalize_super(super_name: str) -> str:
|
|
91
|
+
"""Keep the super name if it's a known builtin, else collapse to '?'."""
|
|
92
|
+
if super_name in BUILTIN_TYPES:
|
|
93
|
+
return super_name
|
|
94
|
+
return "?"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class ClassNode:
|
|
99
|
+
"""One node per class in the graph.
|
|
100
|
+
|
|
101
|
+
Stores intrinsic features (counts, flags) plus typed directed edges
|
|
102
|
+
to/from other ``ClassNode``s in the same graph.
|
|
103
|
+
|
|
104
|
+
Attributes:
|
|
105
|
+
name: Simple class name.
|
|
106
|
+
package: Package / namespace string (empty for default package).
|
|
107
|
+
method_count: Number of instance methods, getters, setters.
|
|
108
|
+
static_method_count: Number of static methods.
|
|
109
|
+
field_count: Number of instance fields.
|
|
110
|
+
static_field_count: Number of static fields.
|
|
111
|
+
super_name: Superclass name, normalized — builtins preserved,
|
|
112
|
+
user classes collapsed to ``"?"``.
|
|
113
|
+
is_interface: Whether the class is an interface.
|
|
114
|
+
is_sealed: Whether the class is sealed (no dynamic properties).
|
|
115
|
+
out_edges: ``(target_name, edge_kind)`` pairs for outgoing refs.
|
|
116
|
+
in_edges: ``(source_name, edge_kind)`` pairs for incoming refs.
|
|
117
|
+
out_degree_by_kind: Per-kind outgoing edge counts.
|
|
118
|
+
in_degree_by_kind: Per-kind incoming edge counts.
|
|
119
|
+
string_pool: String constants referenced by this class.
|
|
120
|
+
method_fps: Method fingerprints for all methods + constructor.
|
|
121
|
+
total_code_size: Sum of bytecode sizes across all fingerprints.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
name: str = ""
|
|
125
|
+
package: str = ""
|
|
126
|
+
|
|
127
|
+
method_count: int = 0
|
|
128
|
+
static_method_count: int = 0
|
|
129
|
+
field_count: int = 0
|
|
130
|
+
static_field_count: int = 0
|
|
131
|
+
super_name: str = "?"
|
|
132
|
+
is_interface: bool = False
|
|
133
|
+
is_sealed: bool = False
|
|
134
|
+
|
|
135
|
+
out_edges: list[tuple[str, str]] = field(default_factory=list)
|
|
136
|
+
in_edges: list[tuple[str, str]] = field(default_factory=list)
|
|
137
|
+
out_degree_by_kind: dict[str, int] = field(
|
|
138
|
+
default_factory=lambda: defaultdict(int))
|
|
139
|
+
in_degree_by_kind: dict[str, int] = field(
|
|
140
|
+
default_factory=lambda: defaultdict(int))
|
|
141
|
+
|
|
142
|
+
string_pool: frozenset[str] = field(default_factory=frozenset)
|
|
143
|
+
|
|
144
|
+
method_fps: list[MethodFingerprint] = field(default_factory=list)
|
|
145
|
+
total_code_size: int = 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class ClassGraph:
|
|
150
|
+
"""Directed graph of class-to-class references with typed edges.
|
|
151
|
+
|
|
152
|
+
Nodes are keyed by simple class name (not qualified).
|
|
153
|
+
|
|
154
|
+
Attributes:
|
|
155
|
+
nodes: Simple class name → :class:`ClassNode`.
|
|
156
|
+
string_to_classes: Reverse index — string literal → set of
|
|
157
|
+
class names that reference that string.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
nodes: dict[str, ClassNode] = field(default_factory=dict)
|
|
161
|
+
string_to_classes: dict[str, set[str]] = field(
|
|
162
|
+
default_factory=lambda: defaultdict(set))
|
|
163
|
+
|
|
164
|
+
def total_degree(self, name: str) -> int:
|
|
165
|
+
"""Total degree (in + out) for a node, or 0 if the node is absent."""
|
|
166
|
+
node = self.nodes.get(name)
|
|
167
|
+
if node is None:
|
|
168
|
+
return 0
|
|
169
|
+
return len(node.out_edges) + len(node.in_edges)
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def from_workspace(cls, workspace: Workspace) -> ClassGraph:
|
|
173
|
+
"""Build a :class:`ClassGraph` from a loaded :class:`Workspace`.
|
|
174
|
+
|
|
175
|
+
Walks every class in the workspace, creates a node with intrinsic
|
|
176
|
+
features, then follows its references to populate typed edges to
|
|
177
|
+
other user-defined classes. Framework/builtin targets and
|
|
178
|
+
self-references are filtered out. Finally, method fingerprints are
|
|
179
|
+
extracted for every class.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
workspace: A :class:`flashkit.workspace.Workspace` with at least
|
|
183
|
+
one loaded SWF.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
A fully populated :class:`ClassGraph`.
|
|
187
|
+
"""
|
|
188
|
+
ref_index = ReferenceIndex.from_workspace(workspace)
|
|
189
|
+
|
|
190
|
+
# Map qualified + simple names → simple name for edge resolution.
|
|
191
|
+
all_class_names: set[str] = set()
|
|
192
|
+
qname_to_name: dict[str, str] = {}
|
|
193
|
+
for info in workspace.classes:
|
|
194
|
+
all_class_names.add(info.name)
|
|
195
|
+
qname_to_name[info.qualified_name] = info.name
|
|
196
|
+
qname_to_name[info.name] = info.name
|
|
197
|
+
|
|
198
|
+
graph = cls()
|
|
199
|
+
|
|
200
|
+
# Step 1: create nodes with intrinsic features.
|
|
201
|
+
for info in workspace.classes:
|
|
202
|
+
node = ClassNode(
|
|
203
|
+
name=info.name,
|
|
204
|
+
package=info.package,
|
|
205
|
+
method_count=len(info.methods),
|
|
206
|
+
static_method_count=len(info.static_methods),
|
|
207
|
+
field_count=len(info.fields),
|
|
208
|
+
static_field_count=len(info.static_fields),
|
|
209
|
+
super_name=_normalize_super(info.super_name),
|
|
210
|
+
is_interface=info.is_interface,
|
|
211
|
+
is_sealed=info.is_sealed,
|
|
212
|
+
)
|
|
213
|
+
graph.nodes[info.name] = node
|
|
214
|
+
|
|
215
|
+
# Step 2: walk references to build edges + string pools.
|
|
216
|
+
class_strings: dict[str, set[str]] = defaultdict(set)
|
|
217
|
+
|
|
218
|
+
for info in workspace.classes:
|
|
219
|
+
refs = ref_index.references_from(info.qualified_name)
|
|
220
|
+
node = graph.nodes[info.name]
|
|
221
|
+
|
|
222
|
+
for ref in refs:
|
|
223
|
+
if ref.ref_kind == "string_use":
|
|
224
|
+
class_strings[info.name].add(ref.target)
|
|
225
|
+
graph.string_to_classes[ref.target].add(info.name)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if ref.ref_kind not in CLASS_EDGE_KINDS:
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
target_name = qname_to_name.get(ref.target)
|
|
232
|
+
if target_name is None:
|
|
233
|
+
if ref.target in all_class_names:
|
|
234
|
+
target_name = ref.target
|
|
235
|
+
else:
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if target_name in FRAMEWORK_TYPES:
|
|
239
|
+
continue
|
|
240
|
+
if target_name == info.name:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
edge = (target_name, ref.ref_kind)
|
|
244
|
+
node.out_edges.append(edge)
|
|
245
|
+
node.out_degree_by_kind[ref.ref_kind] += 1
|
|
246
|
+
|
|
247
|
+
target_node = graph.nodes.get(target_name)
|
|
248
|
+
if target_node is not None:
|
|
249
|
+
target_node.in_edges.append((info.name, ref.ref_kind))
|
|
250
|
+
target_node.in_degree_by_kind[ref.ref_kind] += 1
|
|
251
|
+
|
|
252
|
+
for cls_name, strings in class_strings.items():
|
|
253
|
+
node = graph.nodes.get(cls_name)
|
|
254
|
+
if node is not None:
|
|
255
|
+
node.string_pool = frozenset(strings)
|
|
256
|
+
|
|
257
|
+
# Step 3: extract method fingerprints for each class.
|
|
258
|
+
for info in workspace.classes:
|
|
259
|
+
node = graph.nodes.get(info.name)
|
|
260
|
+
if node is None:
|
|
261
|
+
continue
|
|
262
|
+
try:
|
|
263
|
+
abc = info.abc
|
|
264
|
+
except RuntimeError:
|
|
265
|
+
continue
|
|
266
|
+
fps = extract_all_fingerprints(info, abc)
|
|
267
|
+
node.method_fps = fps
|
|
268
|
+
node.total_code_size = sum(fp.code_size for fp in fps)
|
|
269
|
+
|
|
270
|
+
return graph
|
|
@@ -23,6 +23,10 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
from dataclasses import dataclass, field
|
|
25
25
|
from collections import defaultdict
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from ..workspace.workspace import Workspace
|
|
26
30
|
|
|
27
31
|
from ..abc.types import AbcFile
|
|
28
32
|
from ..abc.disasm import scan_relevant_opcodes
|
|
@@ -91,7 +95,7 @@ class FieldAccessIndex:
|
|
|
91
95
|
self.by_class[access.class_name].append(access)
|
|
92
96
|
|
|
93
97
|
@classmethod
|
|
94
|
-
def from_workspace(cls, workspace:
|
|
98
|
+
def from_workspace(cls, workspace: Workspace) -> FieldAccessIndex:
|
|
95
99
|
"""Build a FieldAccessIndex from a Workspace.
|
|
96
100
|
|
|
97
101
|
Walks all method bodies, decodes instructions, and collects
|
|
@@ -103,8 +107,7 @@ class FieldAccessIndex:
|
|
|
103
107
|
Returns:
|
|
104
108
|
Populated FieldAccessIndex.
|
|
105
109
|
"""
|
|
106
|
-
|
|
107
|
-
ws: Workspace = workspace # type: ignore[assignment]
|
|
110
|
+
ws = workspace
|
|
108
111
|
|
|
109
112
|
index = cls()
|
|
110
113
|
for abc in ws.abc_blocks:
|
|
@@ -21,11 +21,15 @@ Usage::
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
24
25
|
from dataclasses import dataclass, field
|
|
25
26
|
from collections import defaultdict
|
|
26
27
|
|
|
27
28
|
from ..info.class_info import ClassInfo
|
|
28
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from ..workspace.workspace import Workspace
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
@dataclass(slots=True)
|
|
31
35
|
class InheritanceGraph:
|
|
@@ -76,6 +80,20 @@ class InheritanceGraph:
|
|
|
76
80
|
|
|
77
81
|
return graph
|
|
78
82
|
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_workspace(cls, workspace: Workspace) -> InheritanceGraph:
|
|
85
|
+
"""Build an InheritanceGraph from a Workspace's loaded classes.
|
|
86
|
+
|
|
87
|
+
Equivalent to ``InheritanceGraph.from_classes(workspace.classes)``.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
workspace: Workspace instance with loaded classes.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Populated InheritanceGraph.
|
|
94
|
+
"""
|
|
95
|
+
return cls.from_classes(workspace.classes)
|
|
96
|
+
|
|
79
97
|
def get_parent(self, name: str) -> str | None:
|
|
80
98
|
"""Get the direct superclass of a class.
|
|
81
99
|
|