swf-toolkit 0.0.0-stage → 0.1.0

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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -2
  3. package/README.tr.md +126 -0
  4. package/dist/abc/abc-file.d.ts +168 -0
  5. package/dist/abc/abc-file.js +656 -0
  6. package/dist/abc/analysis.d.ts +36 -0
  7. package/dist/abc/analysis.js +213 -0
  8. package/dist/abc/code.d.ts +59 -0
  9. package/dist/abc/code.js +379 -0
  10. package/dist/abc/constants.d.ts +80 -0
  11. package/dist/abc/constants.js +93 -0
  12. package/dist/abc/interner.d.ts +46 -0
  13. package/dist/abc/interner.js +168 -0
  14. package/dist/abc/model.d.ts +40 -0
  15. package/dist/abc/model.js +146 -0
  16. package/dist/abc/opcodes.d.ts +39 -0
  17. package/dist/abc/opcodes.js +196 -0
  18. package/dist/abc/pcode/assembler.d.ts +42 -0
  19. package/dist/abc/pcode/assembler.js +635 -0
  20. package/dist/abc/pcode/disassembler.d.ts +17 -0
  21. package/dist/abc/pcode/disassembler.js +147 -0
  22. package/dist/abc/pcode/format.d.ts +23 -0
  23. package/dist/abc/pcode/format.js +131 -0
  24. package/dist/abc/pcode/lexer.d.ts +34 -0
  25. package/dist/abc/pcode/lexer.js +183 -0
  26. package/dist/assets/assets.d.ts +26 -0
  27. package/dist/assets/assets.js +151 -0
  28. package/dist/assets/images.d.ts +31 -0
  29. package/dist/assets/images.js +238 -0
  30. package/dist/assets/png.d.ts +12 -0
  31. package/dist/assets/png.js +196 -0
  32. package/dist/assets/sounds.d.ts +37 -0
  33. package/dist/assets/sounds.js +236 -0
  34. package/dist/avm1/avm1.d.ts +55 -0
  35. package/dist/avm1/avm1.js +642 -0
  36. package/dist/avm1/project.d.ts +16 -0
  37. package/dist/avm1/project.js +81 -0
  38. package/dist/cjs/abc/abc-file.d.ts +168 -0
  39. package/dist/cjs/abc/abc-file.js +663 -0
  40. package/dist/cjs/abc/analysis.d.ts +36 -0
  41. package/dist/cjs/abc/analysis.js +221 -0
  42. package/dist/cjs/abc/code.d.ts +59 -0
  43. package/dist/cjs/abc/code.js +387 -0
  44. package/dist/cjs/abc/constants.d.ts +80 -0
  45. package/dist/cjs/abc/constants.js +97 -0
  46. package/dist/cjs/abc/interner.d.ts +46 -0
  47. package/dist/cjs/abc/interner.js +172 -0
  48. package/dist/cjs/abc/model.d.ts +40 -0
  49. package/dist/cjs/abc/model.js +155 -0
  50. package/dist/cjs/abc/opcodes.d.ts +39 -0
  51. package/dist/cjs/abc/opcodes.js +201 -0
  52. package/dist/cjs/abc/pcode/assembler.d.ts +42 -0
  53. package/dist/cjs/abc/pcode/assembler.js +640 -0
  54. package/dist/cjs/abc/pcode/disassembler.d.ts +17 -0
  55. package/dist/cjs/abc/pcode/disassembler.js +152 -0
  56. package/dist/cjs/abc/pcode/format.d.ts +23 -0
  57. package/dist/cjs/abc/pcode/format.js +141 -0
  58. package/dist/cjs/abc/pcode/lexer.d.ts +34 -0
  59. package/dist/cjs/abc/pcode/lexer.js +188 -0
  60. package/dist/cjs/assets/assets.d.ts +26 -0
  61. package/dist/cjs/assets/assets.js +159 -0
  62. package/dist/cjs/assets/images.d.ts +31 -0
  63. package/dist/cjs/assets/images.js +246 -0
  64. package/dist/cjs/assets/png.d.ts +12 -0
  65. package/dist/cjs/assets/png.js +202 -0
  66. package/dist/cjs/assets/sounds.d.ts +37 -0
  67. package/dist/cjs/assets/sounds.js +246 -0
  68. package/dist/cjs/avm1/avm1.d.ts +55 -0
  69. package/dist/cjs/avm1/avm1.js +649 -0
  70. package/dist/cjs/avm1/project.d.ts +16 -0
  71. package/dist/cjs/avm1/project.js +88 -0
  72. package/dist/cjs/compiler/ast.d.ts +285 -0
  73. package/dist/cjs/compiler/ast.js +3 -0
  74. package/dist/cjs/compiler/class-compiler.d.ts +34 -0
  75. package/dist/cjs/compiler/class-compiler.js +440 -0
  76. package/dist/cjs/compiler/codegen.d.ts +144 -0
  77. package/dist/cjs/compiler/codegen.js +1589 -0
  78. package/dist/cjs/compiler/lexer.d.ts +27 -0
  79. package/dist/cjs/compiler/lexer.js +254 -0
  80. package/dist/cjs/compiler/parser.d.ts +58 -0
  81. package/dist/cjs/compiler/parser.js +989 -0
  82. package/dist/cjs/compiler/program.d.ts +29 -0
  83. package/dist/cjs/compiler/program.js +36 -0
  84. package/dist/cjs/compiler/scope.d.ts +22 -0
  85. package/dist/cjs/compiler/scope.js +242 -0
  86. package/dist/cjs/compression/lzma.d.ts +37 -0
  87. package/dist/cjs/compression/lzma.js +575 -0
  88. package/dist/cjs/compression/swf-container.d.ts +19 -0
  89. package/dist/cjs/compression/swf-container.js +92 -0
  90. package/dist/cjs/decompiler/ast.d.ts +228 -0
  91. package/dist/cjs/decompiler/ast.js +455 -0
  92. package/dist/cjs/decompiler/cfg.d.ts +40 -0
  93. package/dist/cjs/decompiler/cfg.js +236 -0
  94. package/dist/cjs/decompiler/class-printer.d.ts +9 -0
  95. package/dist/cjs/decompiler/class-printer.js +173 -0
  96. package/dist/cjs/decompiler/method.d.ts +41 -0
  97. package/dist/cjs/decompiler/method.js +436 -0
  98. package/dist/cjs/decompiler/simulate.d.ts +55 -0
  99. package/dist/cjs/decompiler/simulate.js +591 -0
  100. package/dist/cjs/decompiler/structure.d.ts +48 -0
  101. package/dist/cjs/decompiler/structure.js +764 -0
  102. package/dist/cjs/errors.d.ts +11 -0
  103. package/dist/cjs/errors.js +25 -0
  104. package/dist/cjs/index.d.ts +41 -0
  105. package/dist/cjs/index.js +82 -0
  106. package/dist/cjs/io/reader.d.ts +48 -0
  107. package/dist/cjs/io/reader.js +180 -0
  108. package/dist/cjs/io/writer.d.ts +37 -0
  109. package/dist/cjs/io/writer.js +175 -0
  110. package/dist/cjs/package.json +1 -0
  111. package/dist/cjs/project/pcode-project.d.ts +32 -0
  112. package/dist/cjs/project/pcode-project.js +210 -0
  113. package/dist/cjs/project/source-export.d.ts +12 -0
  114. package/dist/cjs/project/source-export.js +51 -0
  115. package/dist/cjs/project/source-import.d.ts +13 -0
  116. package/dist/cjs/project/source-import.js +73 -0
  117. package/dist/cjs/swf/swf.d.ts +63 -0
  118. package/dist/cjs/swf/swf.js +147 -0
  119. package/dist/cjs/swf/tag-codes.d.ts +77 -0
  120. package/dist/cjs/swf/tag-codes.js +109 -0
  121. package/dist/cjs/swf/tags.d.ts +118 -0
  122. package/dist/cjs/swf/tags.js +330 -0
  123. package/dist/cli.d.ts +2 -0
  124. package/dist/cli.js +296 -0
  125. package/dist/compiler/ast.d.ts +285 -0
  126. package/dist/compiler/ast.js +2 -0
  127. package/dist/compiler/class-compiler.d.ts +34 -0
  128. package/dist/compiler/class-compiler.js +437 -0
  129. package/dist/compiler/codegen.d.ts +144 -0
  130. package/dist/compiler/codegen.js +1585 -0
  131. package/dist/compiler/lexer.d.ts +27 -0
  132. package/dist/compiler/lexer.js +249 -0
  133. package/dist/compiler/parser.d.ts +58 -0
  134. package/dist/compiler/parser.js +984 -0
  135. package/dist/compiler/program.d.ts +29 -0
  136. package/dist/compiler/program.js +32 -0
  137. package/dist/compiler/scope.d.ts +22 -0
  138. package/dist/compiler/scope.js +238 -0
  139. package/dist/compression/lzma.d.ts +37 -0
  140. package/dist/compression/lzma.js +569 -0
  141. package/dist/compression/swf-container.d.ts +19 -0
  142. package/dist/compression/swf-container.js +87 -0
  143. package/dist/decompiler/ast.d.ts +228 -0
  144. package/dist/decompiler/ast.js +441 -0
  145. package/dist/decompiler/cfg.d.ts +40 -0
  146. package/dist/decompiler/cfg.js +231 -0
  147. package/dist/decompiler/class-printer.d.ts +9 -0
  148. package/dist/decompiler/class-printer.js +170 -0
  149. package/dist/decompiler/method.d.ts +41 -0
  150. package/dist/decompiler/method.js +427 -0
  151. package/dist/decompiler/simulate.d.ts +55 -0
  152. package/dist/decompiler/simulate.js +586 -0
  153. package/dist/decompiler/structure.d.ts +48 -0
  154. package/dist/decompiler/structure.js +760 -0
  155. package/dist/errors.d.ts +11 -0
  156. package/dist/errors.js +20 -0
  157. package/dist/index.d.ts +41 -0
  158. package/dist/index.js +49 -0
  159. package/dist/io/reader.d.ts +48 -0
  160. package/dist/io/reader.js +176 -0
  161. package/dist/io/writer.d.ts +37 -0
  162. package/dist/io/writer.js +169 -0
  163. package/dist/project/pcode-project.d.ts +32 -0
  164. package/dist/project/pcode-project.js +203 -0
  165. package/dist/project/source-export.d.ts +12 -0
  166. package/dist/project/source-export.js +47 -0
  167. package/dist/project/source-import.d.ts +13 -0
  168. package/dist/project/source-import.js +70 -0
  169. package/dist/swf/swf.d.ts +63 -0
  170. package/dist/swf/swf.js +143 -0
  171. package/dist/swf/tag-codes.d.ts +77 -0
  172. package/dist/swf/tag-codes.js +105 -0
  173. package/dist/swf/tags.d.ts +118 -0
  174. package/dist/swf/tags.js +316 -0
  175. package/package.json +71 -4
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Export every class of an SWF to editable `.pcode` files and import the
3
+ * edited files back. Only methods whose text actually changed are
4
+ * re-assembled, so untouched code stays byte-identical.
5
+ */
6
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
+ import { dirname, join, relative, sep } from 'node:path';
8
+ import { splitQualifiedName } from '../abc/abc-file.js';
9
+ import { Interner } from '../abc/interner.js';
10
+ import { classMethods, describeMethodRef, scriptMethods, scriptOfClass } from '../abc/model.js';
11
+ import { assembleMethods } from '../abc/pcode/assembler.js';
12
+ import { disassembleMethod } from '../abc/pcode/disassembler.js';
13
+ import { formatMultiname } from '../abc/pcode/format.js';
14
+ import { tokenizeLine } from '../abc/pcode/lexer.js';
15
+ import { AssemblerError } from '../errors.js';
16
+ export const MANIFEST_NAME = 'swf-toolkit.json';
17
+ /** Make a class / package name safe as a path segment on all platforms. */
18
+ export function safeSegment(s) {
19
+ if (s === '' || s === '.' || s === '..')
20
+ return `_${s.length}_`;
21
+ let out = '';
22
+ for (const ch of s) {
23
+ const cp = ch.codePointAt(0);
24
+ if (/[\\/:*?"<>|%]/.test(ch) || cp < 0x20 || cp === 0x7f || (cp >= 0xd800 && cp <= 0xdfff))
25
+ out += `%${cp.toString(16).padStart(2, '0')}`;
26
+ else
27
+ out += ch;
28
+ }
29
+ if (/^(con|prn|aux|nul|com\d|lpt\d)$/i.test(out))
30
+ out = `%${out}`;
31
+ if (/[. ]$/.test(out))
32
+ out += '%';
33
+ return out.length > 180 ? out.slice(0, 160) + '%' + hash(s) : out;
34
+ }
35
+ function hash(s) {
36
+ let h = 2166136261;
37
+ for (let i = 0; i < s.length; i++)
38
+ h = Math.imul(h ^ s.charCodeAt(i), 16777619);
39
+ return (h >>> 0).toString(16);
40
+ }
41
+ function classFilePath(className) {
42
+ const { pkg, name } = splitQualifiedName(className);
43
+ const parts = pkg ? pkg.split('.').map(safeSegment) : [];
44
+ return [...parts, safeSegment(name) + '.pcode'].join('/');
45
+ }
46
+ function renderFile(abc, header, owner, refs, done) {
47
+ const chunks = [header.map((h) => `; ${h}`).join('\n') + '\n'];
48
+ const methods = [];
49
+ for (const r of refs) {
50
+ if (done.has(r.methodIndex) || !abc.methods[r.methodIndex])
51
+ continue;
52
+ done.add(r.methodIndex);
53
+ methods.push(r.methodIndex);
54
+ chunks.push(disassembleMethod(abc, r.methodIndex, { title: describeMethodRef(abc, owner, r) }));
55
+ }
56
+ return { text: chunks.join('\n'), methods };
57
+ }
58
+ /** Writes one `.pcode` file per class (plus a manifest) into `outDir`. */
59
+ export async function exportPcode(swf, outDir) {
60
+ const tags = swf.abcTags;
61
+ const manifest = { format: 'swf-toolkit-pcode', version: 1, abcs: [], files: {} };
62
+ const written = [];
63
+ let methodCount = 0;
64
+ for (let k = 0; k < tags.length; k++) {
65
+ const tag = tags[k];
66
+ const abc = tag.abc;
67
+ const base = tags.length > 1 ? `_abc${k}_${safeSegment(tag.abcName || 'unnamed')}` : '';
68
+ manifest.abcs.push({ name: tag.abcName, dir: base });
69
+ const done = new Set();
70
+ const used = new Set();
71
+ const unique = (p) => {
72
+ let candidate = p;
73
+ for (let n = 2; used.has(candidate.toLowerCase()); n++)
74
+ candidate = p.replace(/\.pcode$/, `~${n}.pcode`);
75
+ used.add(candidate.toLowerCase());
76
+ return candidate;
77
+ };
78
+ const emit = async (rel, text, entry) => {
79
+ const path = base ? `${base}/${rel}` : rel;
80
+ const abs = join(outDir, ...path.split('/'));
81
+ await mkdir(dirname(abs), { recursive: true });
82
+ await writeFile(abs, text, 'utf8');
83
+ manifest.files[path] = entry;
84
+ written.push(abs);
85
+ methodCount += entry.methods.length;
86
+ };
87
+ const scriptDone = new Set();
88
+ for (let c = 0; c < abc.instances.length; c++) {
89
+ const name = abc.className(c);
90
+ const inst = abc.instances[c];
91
+ const header = [
92
+ `class ${name} (class index ${c})`,
93
+ `extends ${inst.superName ? formatMultiname(abc, inst.superName) : '-'}`,
94
+ ...(inst.interfaces.length ? [`implements ${inst.interfaces.map((i) => formatMultiname(abc, i)).join(', ')}`] : []),
95
+ 'Edit method blocks and run import; blocks you do not change are left untouched.',
96
+ ];
97
+ const refs = [];
98
+ const s = scriptOfClass(abc, c);
99
+ if (s >= 0 && !scriptDone.has(s)) {
100
+ scriptDone.add(s);
101
+ refs.push(...scriptMethods(abc, s, false));
102
+ }
103
+ refs.push(...classMethods(abc, c));
104
+ const { text, methods } = renderFile(abc, header, name, refs, done);
105
+ await emit(unique(classFilePath(name)), text, { abc: k, className: name, methods });
106
+ }
107
+ for (let s = 0; s < abc.scripts.length; s++) {
108
+ if (scriptDone.has(s))
109
+ continue;
110
+ const refs = scriptMethods(abc, s);
111
+ const { text, methods } = renderFile(abc, [`script ${s}`], `script${s}`, refs, done);
112
+ if (methods.length)
113
+ await emit(unique(`_scripts/script_${s}.pcode`), text, { abc: k, methods });
114
+ }
115
+ // Anything not reachable from classes or scripts.
116
+ const rest = [];
117
+ for (let m = 0; m < abc.methods.length; m++)
118
+ if (!done.has(m))
119
+ rest.push({ methodIndex: m, role: 'closure', name: abc.strings[abc.methods[m].name] ?? '', isStatic: false });
120
+ if (rest.length) {
121
+ const { text, methods } = renderFile(abc, ['methods not referenced by any class or script'], '<orphan>', rest, done);
122
+ await emit(unique('_scripts/orphans.pcode'), text, { abc: k, methods });
123
+ }
124
+ }
125
+ const manifestPath = join(outDir, MANIFEST_NAME);
126
+ await mkdir(outDir, { recursive: true });
127
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
128
+ return { outDir, files: written, methodCount };
129
+ }
130
+ /** Splits a P-code document into `method ... end` blocks with their method index. */
131
+ export function splitMethodBlocks(text) {
132
+ const lines = text.split('\n');
133
+ const blocks = [];
134
+ let start = -1;
135
+ let index = 'new';
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const m = /^\s*method\s+(\d+|new)\b/.exec(lines[i]);
138
+ if (m && start < 0) {
139
+ start = i;
140
+ index = m[1] === 'new' ? 'new' : Number(m[1]);
141
+ }
142
+ else if (start >= 0 && /^\s*end\s*(;.*)?$/.test(lines[i])) {
143
+ // Pad with blank lines so assembler line numbers match the file.
144
+ blocks.push({ text: '\n'.repeat(start) + lines.slice(start, i + 1).join('\n'), startLine: start + 1, methodIndex: index });
145
+ start = -1;
146
+ }
147
+ }
148
+ if (start >= 0)
149
+ throw new AssemblerError("Missing 'end'", lines.length, 1);
150
+ return blocks;
151
+ }
152
+ /** Token-level normalisation: ignores comments, indentation and spacing. */
153
+ function normalize(text) {
154
+ return text
155
+ .split('\n')
156
+ .map((l, i) => tokenizeLine(l, i + 1).map((t) => t.text).join(' '))
157
+ .filter((l) => l.length > 0)
158
+ .join('\n');
159
+ }
160
+ /** Applies edited `.pcode` files from `dir` (created by {@link exportPcode}) to `swf`. */
161
+ export async function importPcode(swf, dir, options = {}) {
162
+ const manifest = JSON.parse(await readFile(join(dir, MANIFEST_NAME), 'utf8'));
163
+ if (manifest.format !== 'swf-toolkit-pcode')
164
+ throw new Error(`${MANIFEST_NAME} is not a swf-toolkit P-code manifest`);
165
+ const tags = swf.abcTags;
166
+ const interners = new Map();
167
+ const result = { changed: [], unchanged: 0 };
168
+ const only = options.files ? new Set(options.files.map((f) => f.split(sep).join('/'))) : undefined;
169
+ for (const [rel, entry] of Object.entries(manifest.files)) {
170
+ if (only && !only.has(rel))
171
+ continue;
172
+ const tag = tags[entry.abc];
173
+ if (!tag)
174
+ throw new Error(`${rel}: ABC tag #${entry.abc} not found in SWF`);
175
+ const abc = tag.abc;
176
+ const path = join(dir, ...rel.split('/'));
177
+ let text;
178
+ try {
179
+ text = await readFile(path, 'utf8');
180
+ }
181
+ catch {
182
+ continue; // file deleted: leave methods as they are
183
+ }
184
+ for (const block of splitMethodBlocks(text)) {
185
+ if (block.methodIndex !== 'new') {
186
+ const current = abc.methods[block.methodIndex] ? disassembleMethod(abc, block.methodIndex) : '';
187
+ if (normalize(current) === normalize(block.text)) {
188
+ result.unchanged++;
189
+ continue;
190
+ }
191
+ }
192
+ let interner = interners.get(entry.abc);
193
+ if (!interner) {
194
+ interner = new Interner(abc);
195
+ interners.set(entry.abc, interner);
196
+ }
197
+ const assembled = assembleMethods(abc, block.text, { source: relative(process.cwd(), path), interner });
198
+ for (const a of assembled)
199
+ result.changed.push({ file: rel, methodIndex: a.methodIndex });
200
+ }
201
+ }
202
+ return result;
203
+ }
@@ -0,0 +1,12 @@
1
+ import type { Swf } from '../swf/swf.js';
2
+ /** First-line marker identifying the exact class an exported file belongs to. */
3
+ export declare const CLASS_MARKER = "// swf-toolkit:";
4
+ export interface ExportSourcesResult {
5
+ files: string[];
6
+ /** Classes whose decompilation threw (a stub with the error was written). */
7
+ failed: string[];
8
+ }
9
+ /** Writes decompiled `.as` files (one per class) into `outDir`. */
10
+ export declare function exportSources(swf: Swf, outDir: string, options?: {
11
+ classes?: string[];
12
+ }): Promise<ExportSourcesResult>;
@@ -0,0 +1,47 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { splitQualifiedName } from '../abc/abc-file.js';
4
+ import { decompileClass } from '../decompiler/class-printer.js';
5
+ import { safeSegment } from './pcode-project.js';
6
+ /** First-line marker identifying the exact class an exported file belongs to. */
7
+ export const CLASS_MARKER = '// swf-toolkit:';
8
+ /** Writes decompiled `.as` files (one per class) into `outDir`. */
9
+ export async function exportSources(swf, outDir, options = {}) {
10
+ const files = [];
11
+ const failed = [];
12
+ const used = new Set();
13
+ const only = options.classes ? new Set(options.classes) : undefined;
14
+ const tags = swf.abcTags;
15
+ for (let k = 0; k < tags.length; k++) {
16
+ const abc = tags[k].abc;
17
+ const counts = new Map();
18
+ for (let c = 0; c < abc.instances.length; c++)
19
+ counts.set(abc.className(c), (counts.get(abc.className(c)) ?? 0) + 1);
20
+ for (let c = 0; c < abc.instances.length; c++) {
21
+ const name = abc.className(c);
22
+ if (only && !only.has(name))
23
+ continue;
24
+ const { pkg, name: short } = splitQualifiedName(name);
25
+ let rel = [...(pkg ? pkg.split('.').map(safeSegment) : []), safeSegment(short) + '.as'].join('/');
26
+ for (let n = 2; used.has(rel.toLowerCase()); n++)
27
+ rel = rel.replace(/(~\d+)?\.as$/, `~${n}.as`);
28
+ used.add(rel.toLowerCase());
29
+ let text;
30
+ try {
31
+ text = decompileClass(abc, c);
32
+ }
33
+ catch (e) {
34
+ failed.push(name);
35
+ text = `// Failed to decompile ${name}: ${e.message}\n`;
36
+ }
37
+ // Ambiguous names (duplicate classes, several ABC tags) get a marker so import finds the right class.
38
+ if (counts.get(name) > 1 || tags.length > 1)
39
+ text = `${CLASS_MARKER} abc=${k} class=${c}\n${text}`;
40
+ const abs = join(outDir, ...rel.split('/'));
41
+ await mkdir(dirname(abs), { recursive: true });
42
+ await writeFile(abs, text, 'utf8');
43
+ files.push(abs);
44
+ }
45
+ }
46
+ return { files, failed };
47
+ }
@@ -0,0 +1,13 @@
1
+ import { type CompiledMember } from '../compiler/class-compiler.js';
2
+ import type { Swf } from '../swf/swf.js';
3
+ export interface ImportSourcesResult {
4
+ changes: CompiledMember[];
5
+ /** Files that were parsed and matched no change. */
6
+ unchanged: number;
7
+ }
8
+ export interface ImportSourcesOptions {
9
+ /** Only import these files (absolute, or relative to `dir`). */
10
+ files?: string[];
11
+ }
12
+ /** Compiles `.as` files from `dir` (or explicit file paths) into the SWF. */
13
+ export declare function importSources(swf: Swf, dir: string, options?: ImportSourcesOptions): Promise<ImportSourcesResult>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Applies edited / new `.as` files to an SWF. Classes are matched by their
3
+ * fully qualified name; within existing classes only members whose text
4
+ * differs from the decompiled original are recompiled.
5
+ */
6
+ import { readdir, readFile } from 'node:fs/promises';
7
+ import { join, relative } from 'node:path';
8
+ import { Interner } from '../abc/interner.js';
9
+ import { compileClassSource } from '../compiler/class-compiler.js';
10
+ import { parseAs3 } from '../compiler/parser.js';
11
+ import { CLASS_MARKER } from './source-export.js';
12
+ async function listAsFiles(dir) {
13
+ const out = [];
14
+ const walk = async (d) => {
15
+ for (const e of await readdir(d, { withFileTypes: true })) {
16
+ const p = join(d, e.name);
17
+ if (e.isDirectory())
18
+ await walk(p);
19
+ else if (e.name.endsWith('.as'))
20
+ out.push(p);
21
+ }
22
+ };
23
+ await walk(dir);
24
+ return out.sort();
25
+ }
26
+ /** Compiles `.as` files from `dir` (or explicit file paths) into the SWF. */
27
+ export async function importSources(swf, dir, options = {}) {
28
+ const files = options.files ? options.files.map((f) => (f.startsWith('/') ? f : join(dir, f))) : await listAsFiles(dir);
29
+ const tags = swf.abcTags;
30
+ if (!tags.length)
31
+ throw new Error('SWF has no ActionScript 3 code (no DoABC tag)');
32
+ const interners = new Map();
33
+ const result = { changes: [], unchanged: 0 };
34
+ const existing = [];
35
+ const added = [];
36
+ for (const file of files) {
37
+ const text = await readFile(file, 'utf8');
38
+ const marker = new RegExp(`^${CLASS_MARKER.replace(/[/]/g, '\\/')} abc=(\\d+) class=(\\d+)`).exec(text);
39
+ if (marker) {
40
+ const tag = tags[Number(marker[1])];
41
+ if (!tag)
42
+ throw new Error(`${file}: ABC tag #${marker[1]} does not exist`);
43
+ existing.push({ file, text, tag, classIndex: Number(marker[2]) });
44
+ continue;
45
+ }
46
+ const unit = parseAs3(text, relative(process.cwd(), file));
47
+ let target;
48
+ for (const cls of unit.classes) {
49
+ const q = unit.package ? `${unit.package}.${cls.name}` : cls.name;
50
+ target ??= tags.find((t) => t.abc.findClass(q) >= 0);
51
+ }
52
+ if (target)
53
+ existing.push({ file, text, tag: target });
54
+ else
55
+ added.push({ file, text, tag: tags[tags.length - 1] });
56
+ }
57
+ // Interfaces and base classes first among new files.
58
+ added.sort((a, b) => Number(/\binterface\b/.test(b.text)) - Number(/\binterface\b/.test(a.text)));
59
+ for (const p of [...existing, ...added]) {
60
+ let pool = interners.get(p.tag);
61
+ if (!pool)
62
+ interners.set(p.tag, (pool = new Interner(p.tag.abc)));
63
+ const r = compileClassSource(p.tag.abc, p.text, { source: relative(process.cwd(), p.file), interner: pool, classIndex: p.classIndex });
64
+ if (r.changes.length)
65
+ result.changes.push(...r.changes);
66
+ else
67
+ result.unchanged++;
68
+ }
69
+ return result;
70
+ }
@@ -0,0 +1,63 @@
1
+ import { type PackOptions, type SwfCompression } from '../compression/swf-container.js';
2
+ import { DefineSpriteTag, DoABCTag, Tag } from './tags.js';
3
+ /** Rectangle in twips (1/20 pixel). */
4
+ export interface Rect {
5
+ xMin: number;
6
+ xMax: number;
7
+ yMin: number;
8
+ yMax: number;
9
+ }
10
+ export interface SwfSaveOptions extends PackOptions {
11
+ /** Override the compression used when saving. Defaults to the original compression. */
12
+ compression?: SwfCompression;
13
+ }
14
+ /** An SWF file: header plus a list of tags. */
15
+ export declare class Swf {
16
+ version: number;
17
+ compression: SwfCompression;
18
+ /** Stage bounds in twips. */
19
+ frameSize: Rect;
20
+ /** Frames per second (8.8 fixed point in the file). */
21
+ frameRate: number;
22
+ frameCount: number;
23
+ tags: Tag[];
24
+ /** Bytes after the End tag (normally empty). Preserved on save. */
25
+ trailing: Uint8Array;
26
+ private rectBits;
27
+ constructor(init?: {
28
+ version?: number;
29
+ compression?: SwfCompression;
30
+ frameSize?: Rect;
31
+ frameRate?: number;
32
+ frameCount?: number;
33
+ tags?: Tag[];
34
+ });
35
+ /** Parse an SWF file (FWS, CWS or ZWS). */
36
+ static parse(bytes: Uint8Array): Swf;
37
+ static load(path: string): Promise<Swf>;
38
+ /** Uncompressed SWF body (everything after the 8-byte file header). */
39
+ encodeBody(): Uint8Array;
40
+ /** Serialise to a complete SWF file. */
41
+ toBytes(options?: SwfSaveOptions): Uint8Array;
42
+ /**
43
+ * Write to disk. Uses a temporary file + rename, so saving over the source
44
+ * file is safe.
45
+ */
46
+ save(path: string, options?: SwfSaveOptions): Promise<void>;
47
+ get width(): number;
48
+ get height(): number;
49
+ /** Depth-first iteration over all tags, including tags nested in DefineSprite. */
50
+ walkTags(): Generator<{
51
+ tag: Tag;
52
+ parent: DefineSpriteTag | null;
53
+ index: number;
54
+ }>;
55
+ /** All DoABC / DoABC2 tags in file order. */
56
+ get abcTags(): DoABCTag[];
57
+ /** Character-defining tag with the given id (searches nested sprites too). */
58
+ findCharacter(id: number): Tag | undefined;
59
+ /** Merged SymbolClass mapping: class name → character id. */
60
+ symbolClasses(): Map<string, number>;
61
+ /** Main document class (SymbolClass entry with id 0), if any. */
62
+ get documentClass(): string | undefined;
63
+ }
@@ -0,0 +1,143 @@
1
+ import { readFile, writeFile, rename } from 'node:fs/promises';
2
+ import { dirname, basename, join } from 'node:path';
3
+ import { ByteReader } from '../io/reader.js';
4
+ import { ByteWriter, signedBits } from '../io/writer.js';
5
+ import { packSwf, unpackSwf } from '../compression/swf-container.js';
6
+ import { DefineSpriteTag, DoABCTag, readTags, SymbolTableTag, writeTags } from './tags.js';
7
+ import { TagCode } from './tag-codes.js';
8
+ function readRect(r) {
9
+ const bits = r.ub(5);
10
+ const rect = { xMin: r.sb(bits), xMax: r.sb(bits), yMin: r.sb(bits), yMax: r.sb(bits) };
11
+ r.align();
12
+ return { rect, bits };
13
+ }
14
+ function writeRect(w, rect, minBits = 0) {
15
+ const bits = Math.max(minBits, signedBits(rect.xMin), signedBits(rect.xMax), signedBits(rect.yMin), signedBits(rect.yMax));
16
+ w.ub(5, bits);
17
+ w.sb(bits, rect.xMin);
18
+ w.sb(bits, rect.xMax);
19
+ w.sb(bits, rect.yMin);
20
+ w.sb(bits, rect.yMax);
21
+ w.align();
22
+ }
23
+ /** An SWF file: header plus a list of tags. */
24
+ export class Swf {
25
+ version;
26
+ compression;
27
+ /** Stage bounds in twips. */
28
+ frameSize;
29
+ /** Frames per second (8.8 fixed point in the file). */
30
+ frameRate;
31
+ frameCount;
32
+ tags;
33
+ /** Bytes after the End tag (normally empty). Preserved on save. */
34
+ trailing;
35
+ rectBits;
36
+ constructor(init = {}) {
37
+ this.version = init.version ?? 10;
38
+ this.compression = init.compression ?? 'zlib';
39
+ this.frameSize = init.frameSize ?? { xMin: 0, xMax: 550 * 20, yMin: 0, yMax: 400 * 20 };
40
+ this.frameRate = init.frameRate ?? 24;
41
+ this.frameCount = init.frameCount ?? 1;
42
+ this.tags = init.tags ?? [];
43
+ this.trailing = new Uint8Array(0);
44
+ this.rectBits = 0;
45
+ }
46
+ // -------------------------------------------------------------------------
47
+ // Loading / saving
48
+ // -------------------------------------------------------------------------
49
+ /** Parse an SWF file (FWS, CWS or ZWS). */
50
+ static parse(bytes) {
51
+ const { compression, version, body } = unpackSwf(bytes);
52
+ const r = new ByteReader(body);
53
+ const { rect, bits } = readRect(r);
54
+ const frameRate = r.u16() / 256;
55
+ const frameCount = r.u16();
56
+ const { tags, end } = readTags(body, r.pos, body.length);
57
+ const swf = new Swf({ version, compression, frameSize: rect, frameRate, frameCount, tags });
58
+ swf.rectBits = bits;
59
+ swf.trailing = body.slice(end);
60
+ return swf;
61
+ }
62
+ static async load(path) {
63
+ return Swf.parse(new Uint8Array(await readFile(path)));
64
+ }
65
+ /** Uncompressed SWF body (everything after the 8-byte file header). */
66
+ encodeBody() {
67
+ const w = new ByteWriter(1 << 16);
68
+ writeRect(w, this.frameSize, this.rectBits);
69
+ w.u16(Math.round(this.frameRate * 256));
70
+ w.u16(this.frameCount);
71
+ const tags = this.tags;
72
+ writeTags(w, tags);
73
+ if (tags.length === 0 || tags[tags.length - 1].code !== TagCode.End) {
74
+ w.u16(0); // End tag
75
+ }
76
+ w.bytes(this.trailing);
77
+ return w.toBytes();
78
+ }
79
+ /** Serialise to a complete SWF file. */
80
+ toBytes(options = {}) {
81
+ return packSwf({ compression: options.compression ?? this.compression, version: this.version, body: this.encodeBody() }, options);
82
+ }
83
+ /**
84
+ * Write to disk. Uses a temporary file + rename, so saving over the source
85
+ * file is safe.
86
+ */
87
+ async save(path, options = {}) {
88
+ const bytes = this.toBytes(options);
89
+ const tmp = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
90
+ await writeFile(tmp, bytes);
91
+ await rename(tmp, path);
92
+ }
93
+ // -------------------------------------------------------------------------
94
+ // Convenience accessors
95
+ // -------------------------------------------------------------------------
96
+ get width() {
97
+ return (this.frameSize.xMax - this.frameSize.xMin) / 20;
98
+ }
99
+ get height() {
100
+ return (this.frameSize.yMax - this.frameSize.yMin) / 20;
101
+ }
102
+ /** Depth-first iteration over all tags, including tags nested in DefineSprite. */
103
+ *walkTags() {
104
+ function* walk(list, parent) {
105
+ for (let i = 0; i < list.length; i++) {
106
+ const tag = list[i];
107
+ yield { tag, parent, index: i };
108
+ if (tag instanceof DefineSpriteTag)
109
+ yield* walk(tag.tags, tag);
110
+ }
111
+ }
112
+ yield* walk(this.tags, null);
113
+ }
114
+ /** All DoABC / DoABC2 tags in file order. */
115
+ get abcTags() {
116
+ return this.tags.filter((t) => t instanceof DoABCTag);
117
+ }
118
+ /** Character-defining tag with the given id (searches nested sprites too). */
119
+ findCharacter(id) {
120
+ for (const { tag } of this.walkTags())
121
+ if (tag.characterId === id)
122
+ return tag;
123
+ return undefined;
124
+ }
125
+ /** Merged SymbolClass mapping: class name → character id. */
126
+ symbolClasses() {
127
+ const map = new Map();
128
+ for (const t of this.tags) {
129
+ if (t instanceof SymbolTableTag && t.code === TagCode.SymbolClass) {
130
+ for (const s of t.symbols)
131
+ map.set(s.name, s.id);
132
+ }
133
+ }
134
+ return map;
135
+ }
136
+ /** Main document class (SymbolClass entry with id 0), if any. */
137
+ get documentClass() {
138
+ for (const [name, id] of this.symbolClasses())
139
+ if (id === 0)
140
+ return name;
141
+ return undefined;
142
+ }
143
+ }
@@ -0,0 +1,77 @@
1
+ /** SWF tag codes (SWF File Format Specification v19). */
2
+ export declare const TagCode: {
3
+ readonly End: 0;
4
+ readonly ShowFrame: 1;
5
+ readonly DefineShape: 2;
6
+ readonly FreeCharacter: 3;
7
+ readonly PlaceObject: 4;
8
+ readonly RemoveObject: 5;
9
+ readonly DefineBits: 6;
10
+ readonly DefineButton: 7;
11
+ readonly JPEGTables: 8;
12
+ readonly SetBackgroundColor: 9;
13
+ readonly DefineFont: 10;
14
+ readonly DefineText: 11;
15
+ readonly DoAction: 12;
16
+ readonly DefineFontInfo: 13;
17
+ readonly DefineSound: 14;
18
+ readonly StartSound: 15;
19
+ readonly DefineButtonSound: 17;
20
+ readonly SoundStreamHead: 18;
21
+ readonly SoundStreamBlock: 19;
22
+ readonly DefineBitsLossless: 20;
23
+ readonly DefineBitsJPEG2: 21;
24
+ readonly DefineShape2: 22;
25
+ readonly DefineButtonCxform: 23;
26
+ readonly Protect: 24;
27
+ readonly PlaceObject2: 26;
28
+ readonly RemoveObject2: 28;
29
+ readonly DefineShape3: 32;
30
+ readonly DefineText2: 33;
31
+ readonly DefineButton2: 34;
32
+ readonly DefineBitsJPEG3: 35;
33
+ readonly DefineBitsLossless2: 36;
34
+ readonly DefineEditText: 37;
35
+ readonly DefineSprite: 39;
36
+ readonly ProductInfo: 41;
37
+ readonly FrameLabel: 43;
38
+ readonly SoundStreamHead2: 45;
39
+ readonly DefineMorphShape: 46;
40
+ readonly DefineFont2: 48;
41
+ readonly ExportAssets: 56;
42
+ readonly ImportAssets: 57;
43
+ readonly EnableDebugger: 58;
44
+ readonly DoInitAction: 59;
45
+ readonly DefineVideoStream: 60;
46
+ readonly VideoFrame: 61;
47
+ readonly DefineFontInfo2: 62;
48
+ readonly DebugID: 63;
49
+ readonly EnableDebugger2: 64;
50
+ readonly ScriptLimits: 65;
51
+ readonly SetTabIndex: 66;
52
+ readonly FileAttributes: 69;
53
+ readonly PlaceObject3: 70;
54
+ readonly ImportAssets2: 71;
55
+ readonly DoABC: 72;
56
+ readonly DefineFontAlignZones: 73;
57
+ readonly CSMTextSettings: 74;
58
+ readonly DefineFont3: 75;
59
+ readonly SymbolClass: 76;
60
+ readonly Metadata: 77;
61
+ readonly DefineScalingGrid: 78;
62
+ readonly DoABC2: 82;
63
+ readonly DefineShape4: 83;
64
+ readonly DefineMorphShape2: 84;
65
+ readonly DefineSceneAndFrameLabelData: 86;
66
+ readonly DefineBinaryData: 87;
67
+ readonly DefineFontName: 88;
68
+ readonly StartSound2: 89;
69
+ readonly DefineBitsJPEG4: 90;
70
+ readonly DefineFont4: 91;
71
+ readonly EnableTelemetry: 93;
72
+ readonly PlaceObject4: 94;
73
+ };
74
+ export type TagCodeName = keyof typeof TagCode;
75
+ export declare function tagName(code: number): string;
76
+ /** Tags whose body starts with the u16 id of the character they define. */
77
+ export declare const CHARACTER_DEFINING_TAGS: ReadonlySet<number>;