@swf2es/format 0.0.0-stage → 0.1.1
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.
- package/LICENSE +202 -0
- package/LICENSE.third-party +35 -0
- package/README.md +22 -2
- package/dist/abc-names.d.ts +3 -0
- package/dist/abc-names.d.ts.map +1 -0
- package/dist/abc-names.js +127 -0
- package/dist/abc-names.js.map +1 -0
- package/dist/actions.d.ts +106 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +223 -0
- package/dist/actions.js.map +1 -0
- package/dist/air-descriptor.d.ts +11 -0
- package/dist/air-descriptor.d.ts.map +1 -0
- package/dist/air-descriptor.js +64 -0
- package/dist/air-descriptor.js.map +1 -0
- package/dist/api-versions.d.ts +12 -0
- package/dist/api-versions.d.ts.map +1 -0
- package/dist/api-versions.js +23 -0
- package/dist/api-versions.js.map +1 -0
- package/dist/bitmap.d.ts +34 -0
- package/dist/bitmap.d.ts.map +1 -0
- package/dist/bitmap.js +200 -0
- package/dist/bitmap.js.map +1 -0
- package/dist/button.d.ts +39 -0
- package/dist/button.d.ts.map +1 -0
- package/dist/button.js +61 -0
- package/dist/button.js.map +1 -0
- package/dist/compression.d.ts +47 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +201 -0
- package/dist/compression.js.map +1 -0
- package/dist/display.d.ts +106 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +306 -0
- package/dist/display.js.map +1 -0
- package/dist/filters.d.ts +66 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +137 -0
- package/dist/filters.js.map +1 -0
- package/dist/font.d.ts +60 -0
- package/dist/font.d.ts.map +1 -0
- package/dist/font.js +169 -0
- package/dist/font.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/pbj.d.ts +178 -0
- package/dist/pbj.d.ts.map +1 -0
- package/dist/pbj.js +485 -0
- package/dist/pbj.js.map +1 -0
- package/dist/shape.d.ts +152 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +412 -0
- package/dist/shape.js.map +1 -0
- package/dist/sound.d.ts +71 -0
- package/dist/sound.d.ts.map +1 -0
- package/dist/sound.js +92 -0
- package/dist/sound.js.map +1 -0
- package/dist/swf.d.ts +77 -0
- package/dist/swf.d.ts.map +1 -0
- package/dist/swf.js +164 -0
- package/dist/swf.js.map +1 -0
- package/dist/tags.d.ts +57 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +59 -0
- package/dist/tags.js.map +1 -0
- package/dist/text.d.ts +75 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +136 -0
- package/dist/text.js.map +1 -0
- package/dist/zip.d.ts +17 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +92 -0
- package/dist/zip.js.map +1 -0
- package/package.json +33 -3
- package/src/abc-names.ts +138 -0
- package/src/actions.ts +319 -0
- package/src/air-descriptor.ts +84 -0
- package/src/api-versions.ts +27 -0
- package/src/bitmap.ts +259 -0
- package/src/button.ts +99 -0
- package/src/compression.ts +243 -0
- package/src/display.ts +419 -0
- package/src/filters.ts +206 -0
- package/src/font.ts +238 -0
- package/src/index.ts +151 -0
- package/src/pbj.ts +651 -0
- package/src/shape.ts +618 -0
- package/src/sound.ts +173 -0
- package/src/swf.ts +234 -0
- package/src/tags.ts +58 -0
- package/src/text.ts +213 -0
- package/src/zip.ts +135 -0
package/dist/zip.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// A ZIP archive's files, as an AIR package (`.air`) and any zip of an
|
|
2
|
+
// application's folder hold them: the central directory read from the
|
|
3
|
+
// archive's end, each file stored or deflated. Enough for what packagers
|
|
4
|
+
// write; ZIP64, encryption and the other methods are refused.
|
|
5
|
+
import { zlibUncompress } from "./compression.js";
|
|
6
|
+
import { utf8 } from "./display.js";
|
|
7
|
+
const END_SIGNATURE = 0x06054b50;
|
|
8
|
+
const CENTRAL_SIGNATURE = 0x02014b50;
|
|
9
|
+
const LOCAL_SIGNATURE = 0x04034b50;
|
|
10
|
+
/** The end record's size without its comment, and the most its comment may add. */
|
|
11
|
+
const END_SIZE = 22;
|
|
12
|
+
const MAX_COMMENT = 0xffff;
|
|
13
|
+
const STORED = 0;
|
|
14
|
+
const DEFLATED = 8;
|
|
15
|
+
export class ZipError extends Error {
|
|
16
|
+
name = "ZipError";
|
|
17
|
+
}
|
|
18
|
+
/** The files of the archive `zip`, in its central directory's order. */
|
|
19
|
+
export function readZip(zip) {
|
|
20
|
+
const view = new DataView(zip.buffer, zip.byteOffset, zip.byteLength);
|
|
21
|
+
const end = findEnd(zip, view);
|
|
22
|
+
const count = view.getUint16(end + 10, true);
|
|
23
|
+
let at = view.getUint32(end + 16, true);
|
|
24
|
+
const entries = [];
|
|
25
|
+
for (let i = 0; i < count; i++) {
|
|
26
|
+
if (at + 46 > zip.length || view.getUint32(at, true) !== CENTRAL_SIGNATURE) {
|
|
27
|
+
throw new ZipError("the central directory is cut short");
|
|
28
|
+
}
|
|
29
|
+
const method = view.getUint16(at + 10, true);
|
|
30
|
+
const time = view.getUint16(at + 12, true);
|
|
31
|
+
const date = view.getUint16(at + 14, true);
|
|
32
|
+
const compressed = view.getUint32(at + 20, true);
|
|
33
|
+
const size = view.getUint32(at + 24, true);
|
|
34
|
+
const nameLength = view.getUint16(at + 28, true);
|
|
35
|
+
const extraLength = view.getUint16(at + 30, true);
|
|
36
|
+
const commentLength = view.getUint16(at + 32, true);
|
|
37
|
+
const local = view.getUint32(at + 42, true);
|
|
38
|
+
const name = utf8(zip.subarray(at + 46, at + 46 + nameLength));
|
|
39
|
+
if (compressed === 0xffffffff || size === 0xffffffff || local === 0xffffffff) {
|
|
40
|
+
throw new ZipError(`${name}: ZIP64 archives are not read`);
|
|
41
|
+
}
|
|
42
|
+
if ((view.getUint16(at + 8, true) & 1) !== 0) {
|
|
43
|
+
throw new ZipError(`${name}: encrypted`);
|
|
44
|
+
}
|
|
45
|
+
if (method !== STORED && method !== DEFLATED) {
|
|
46
|
+
throw new ZipError(`${name}: compression method ${method} is not read`);
|
|
47
|
+
}
|
|
48
|
+
entries.push({
|
|
49
|
+
name,
|
|
50
|
+
size,
|
|
51
|
+
modified: dosTime(date, time),
|
|
52
|
+
bytes: () => entryBytes(zip, view, name, local, method, compressed, size),
|
|
53
|
+
});
|
|
54
|
+
at += 46 + nameLength + extraLength + commentLength;
|
|
55
|
+
}
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Where the end of central directory record starts: the last signature
|
|
60
|
+
* whose comment the archive holds, as some tools leave bytes after it.
|
|
61
|
+
*/
|
|
62
|
+
function findEnd(zip, view) {
|
|
63
|
+
const last = zip.length - END_SIZE;
|
|
64
|
+
for (let at = last; at >= 0 && at >= last - MAX_COMMENT; at--) {
|
|
65
|
+
if (view.getUint32(at, true) === END_SIGNATURE &&
|
|
66
|
+
at + END_SIZE + view.getUint16(at + 20, true) <= zip.length) {
|
|
67
|
+
return at;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new ZipError("not a ZIP archive: no end of central directory");
|
|
71
|
+
}
|
|
72
|
+
function entryBytes(zip, view, name, local, method, compressed, size) {
|
|
73
|
+
if (local + 30 > zip.length || view.getUint32(local, true) !== LOCAL_SIGNATURE) {
|
|
74
|
+
throw new ZipError(`${name}: no local header where the directory says`);
|
|
75
|
+
}
|
|
76
|
+
// The local header's own name and extra lengths, which may differ from the directory's.
|
|
77
|
+
const start = local + 30 + view.getUint16(local + 26, true) + view.getUint16(local + 28, true);
|
|
78
|
+
if (start + compressed > zip.length) {
|
|
79
|
+
throw new ZipError(`${name}: cut short`);
|
|
80
|
+
}
|
|
81
|
+
const data = zip.subarray(start, start + compressed);
|
|
82
|
+
const bytes = method === STORED ? data.slice() : zlibUncompress(data, true);
|
|
83
|
+
if (bytes.length !== size) {
|
|
84
|
+
throw new ZipError(`${name}: ${bytes.length} bytes where the directory says ${size}`);
|
|
85
|
+
}
|
|
86
|
+
return bytes;
|
|
87
|
+
}
|
|
88
|
+
/** A DOS date and time (2-second steps) as ms since 1970, read as local time. */
|
|
89
|
+
function dosTime(date, time) {
|
|
90
|
+
return new Date(1980 + (date >> 9), ((date >> 5) & 15) - 1, date & 31, time >> 11, (time >> 5) & 63, (time & 31) * 2).getTime();
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=zip.js.map
|
package/dist/zip.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zip.js","sourceRoot":"","sources":["../src/zip.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swf2es/format",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "SWF, ABC and AVM1 action parsers shared by the compiler and the player",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"swf",
|
|
7
|
+
"flash",
|
|
8
|
+
"abc",
|
|
9
|
+
"avm1",
|
|
10
|
+
"parser"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://swf2es.github.io/swf2es/",
|
|
13
|
+
"bugs": "https://github.com/swf2es/swf2es/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/swf2es/swf2es.git",
|
|
17
|
+
"directory": "packages/format"
|
|
18
|
+
},
|
|
19
|
+
"license": "Apache-2.0 AND (Apache-2.0 OR MIT)",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"!dist/**/.tsbuildinfo",
|
|
30
|
+
"src",
|
|
31
|
+
"LICENSE.third-party"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@swf2es/native": "0.1.1"
|
|
35
|
+
}
|
|
6
36
|
}
|
package/src/abc-names.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The names an ABC refers to, read from its constant pool alone: each
|
|
2
|
+
// qualified name, "package::Name" as avmplus writes a definition's (a
|
|
3
|
+
// top-level one bare), from the pool's QNames in a package's namespace,
|
|
4
|
+
// and from each Multiname, once for every package namespace in its set,
|
|
5
|
+
// as a lookup that names the class unqualified finds it in one of them.
|
|
6
|
+
// What it may name is enough to tell which libraries a SWF expects, as the
|
|
7
|
+
// web element tells an AIR application's SWF from one for the browser.
|
|
8
|
+
import { utf8 } from "./display.js";
|
|
9
|
+
|
|
10
|
+
const CONSTANT_Namespace = 0x08;
|
|
11
|
+
const CONSTANT_PackageNamespace = 0x16;
|
|
12
|
+
const CONSTANT_QName = 0x07;
|
|
13
|
+
const CONSTANT_QNameA = 0x0d;
|
|
14
|
+
const CONSTANT_RTQName = 0x0f;
|
|
15
|
+
const CONSTANT_RTQNameA = 0x10;
|
|
16
|
+
const CONSTANT_RTQNameL = 0x11;
|
|
17
|
+
const CONSTANT_RTQNameLA = 0x12;
|
|
18
|
+
const CONSTANT_Multiname = 0x09;
|
|
19
|
+
const CONSTANT_MultinameA = 0x0e;
|
|
20
|
+
const CONSTANT_MultinameL = 0x1b;
|
|
21
|
+
const CONSTANT_MultinameLA = 0x1c;
|
|
22
|
+
const CONSTANT_TypeName = 0x1d;
|
|
23
|
+
|
|
24
|
+
/** The qualified names `abc` may refer to; what it cannot read stops the reading, keeping what came before. */
|
|
25
|
+
export function abcReferences(abc: Uint8Array): Set<string> {
|
|
26
|
+
const names = new Set<string>();
|
|
27
|
+
let at = 4;
|
|
28
|
+
const u30 = (): number => {
|
|
29
|
+
let value = 0;
|
|
30
|
+
for (let shift = 0; shift < 35; shift += 7) {
|
|
31
|
+
if (at >= abc.length) {
|
|
32
|
+
throw new RangeError("ABC cut short");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const b = abc[at++];
|
|
36
|
+
value += (b & 0x7f) * 2 ** shift;
|
|
37
|
+
if ((b & 0x80) === 0) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
const skipU30s = (count: number) => {
|
|
45
|
+
for (let i = 1; i < count; i++) {
|
|
46
|
+
u30();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
skipU30s(u30()); // ints
|
|
52
|
+
skipU30s(u30()); // uints
|
|
53
|
+
const doubles = u30();
|
|
54
|
+
at += Math.max(0, doubles - 1) * 8;
|
|
55
|
+
// From 47.16 AIR's ABCs have a pool of 4-byte floats next (codegen's parse.ts).
|
|
56
|
+
const minor = abc[0] | (abc[1] << 8);
|
|
57
|
+
const major = abc[2] | (abc[3] << 8);
|
|
58
|
+
if (major === 47 && minor >= 16) {
|
|
59
|
+
const floats = u30();
|
|
60
|
+
at += Math.max(0, floats - 1) * 4;
|
|
61
|
+
}
|
|
62
|
+
const strings: string[] = [""];
|
|
63
|
+
for (let i = 1, n = u30(); i < n; i++) {
|
|
64
|
+
const length = u30();
|
|
65
|
+
strings.push(utf8(abc.subarray(at, at + length)));
|
|
66
|
+
at += length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A package's namespace by its URI; null for any other kind.
|
|
70
|
+
const namespaces: (string | null)[] = [null];
|
|
71
|
+
for (let i = 1, n = u30(); i < n; i++) {
|
|
72
|
+
const kind = abc[at++];
|
|
73
|
+
const name = strings[u30()] ?? "";
|
|
74
|
+
namespaces.push(
|
|
75
|
+
kind === CONSTANT_PackageNamespace || kind === CONSTANT_Namespace ? name : null,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sets: number[][] = [[]];
|
|
80
|
+
for (let i = 1, n = u30(); i < n; i++) {
|
|
81
|
+
const set: number[] = [];
|
|
82
|
+
for (let j = 0, count = u30(); j < count; j++) {
|
|
83
|
+
set.push(u30());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sets.push(set);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const add = (ns: string | null | undefined, name: string | undefined) => {
|
|
90
|
+
if (ns !== null && ns !== undefined && name) {
|
|
91
|
+
names.add(ns === "" ? name : `${ns}::${name}`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
for (let i = 1, n = u30(); i < n; i++) {
|
|
95
|
+
const kind = abc[at++];
|
|
96
|
+
switch (kind) {
|
|
97
|
+
case CONSTANT_QName:
|
|
98
|
+
case CONSTANT_QNameA: {
|
|
99
|
+
const ns = u30();
|
|
100
|
+
add(namespaces[ns], strings[u30()]);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case CONSTANT_RTQName:
|
|
104
|
+
case CONSTANT_RTQNameA:
|
|
105
|
+
u30();
|
|
106
|
+
break;
|
|
107
|
+
case CONSTANT_RTQNameL:
|
|
108
|
+
case CONSTANT_RTQNameLA:
|
|
109
|
+
break;
|
|
110
|
+
case CONSTANT_Multiname:
|
|
111
|
+
case CONSTANT_MultinameA: {
|
|
112
|
+
const name = strings[u30()];
|
|
113
|
+
for (const ns of sets[u30()] ?? []) {
|
|
114
|
+
add(namespaces[ns], name);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case CONSTANT_MultinameL:
|
|
120
|
+
case CONSTANT_MultinameLA:
|
|
121
|
+
u30();
|
|
122
|
+
break;
|
|
123
|
+
case CONSTANT_TypeName: {
|
|
124
|
+
u30();
|
|
125
|
+
skipU30s(u30() + 1);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
// A kind no ABC has: what follows cannot be read.
|
|
130
|
+
return names;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// Cut short: what was read is what it names.
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return names;
|
|
138
|
+
}
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// A SWF's AVM1 action blocks, listed from its tags: DoAction, DoInitAction,
|
|
2
|
+
// a PlaceObject 2 to 4's clip actions and a button's actions, as Ruffle's
|
|
3
|
+
// (https://github.com/ruffle-rs/ruffle) swf/src/read.rs reads them
|
|
4
|
+
// (read_clip_actions, read_clip_event_flags, read_define_button_1 and 2,
|
|
5
|
+
// read_button_action) and its swf/src/types.rs numbers their flags: MIT OR
|
|
6
|
+
// Apache-2.0, Copyright (c) 2018-2022 Ruffle LLC and Ruffle contributors.
|
|
7
|
+
// Where they differ on clip actions, what adl does decides (clipActions, the
|
|
8
|
+
// list's `corrupt`). A malformed button action ends the button's list and
|
|
9
|
+
// keeps those before it: adl defines the button, and its actions cannot run
|
|
10
|
+
// there without a pointer to check them.
|
|
11
|
+
|
|
12
|
+
import { buttonActionsOffset } from "./button.js";
|
|
13
|
+
import { clipActionsOffset, readSprite } from "./display.js";
|
|
14
|
+
import { type Swf, SwfReader, type Tag } from "./swf.js";
|
|
15
|
+
import {
|
|
16
|
+
DefineButton,
|
|
17
|
+
DefineButton2,
|
|
18
|
+
DefineSprite,
|
|
19
|
+
DoAction,
|
|
20
|
+
DoInitAction,
|
|
21
|
+
PlaceObject2,
|
|
22
|
+
PlaceObject3,
|
|
23
|
+
PlaceObject4,
|
|
24
|
+
ShowFrame,
|
|
25
|
+
} from "./tags.js";
|
|
26
|
+
|
|
27
|
+
/** A clip action's events, CLIPEVENTFLAGS' bits; SWF 5 has only the low 16. */
|
|
28
|
+
export const ClipEvent = {
|
|
29
|
+
load: 1 << 0,
|
|
30
|
+
enterFrame: 1 << 1,
|
|
31
|
+
unload: 1 << 2,
|
|
32
|
+
mouseMove: 1 << 3,
|
|
33
|
+
mouseDown: 1 << 4,
|
|
34
|
+
mouseUp: 1 << 5,
|
|
35
|
+
keyDown: 1 << 6,
|
|
36
|
+
keyUp: 1 << 7,
|
|
37
|
+
data: 1 << 8,
|
|
38
|
+
initialize: 1 << 9,
|
|
39
|
+
press: 1 << 10,
|
|
40
|
+
release: 1 << 11,
|
|
41
|
+
releaseOutside: 1 << 12,
|
|
42
|
+
rollOver: 1 << 13,
|
|
43
|
+
rollOut: 1 << 14,
|
|
44
|
+
dragOver: 1 << 15,
|
|
45
|
+
dragOut: 1 << 16,
|
|
46
|
+
keyPress: 1 << 17,
|
|
47
|
+
construct: 1 << 18,
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/** The bits Flash knows; it skips a record with none of them, and only flags of 0 end the list. */
|
|
51
|
+
const CLIP_EVENTS = 0x7ffff;
|
|
52
|
+
|
|
53
|
+
/** A button's state transitions, BUTTONCONDACTION's low 9 bits; its key code is above them. */
|
|
54
|
+
export const ButtonCondition = {
|
|
55
|
+
idleToOverUp: 1 << 0,
|
|
56
|
+
overUpToIdle: 1 << 1,
|
|
57
|
+
overUpToOverDown: 1 << 2,
|
|
58
|
+
overDownToOverUp: 1 << 3,
|
|
59
|
+
overDownToOutDown: 1 << 4,
|
|
60
|
+
outDownToOverDown: 1 << 5,
|
|
61
|
+
outDownToIdle: 1 << 6,
|
|
62
|
+
idleToOverDown: 1 << 7,
|
|
63
|
+
overDownToIdle: 1 << 8,
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What a block belongs to. `timeline` is the sprite whose tags hold it, null
|
|
68
|
+
* for the main timeline's, and `frame` counts that timeline's frames from 0.
|
|
69
|
+
*/
|
|
70
|
+
export type ActionOwner =
|
|
71
|
+
/** DoAction: a frame's actions. */
|
|
72
|
+
| { type: "frame"; timeline: number | null; frame: number }
|
|
73
|
+
/** DoInitAction: `sprite`'s #initclip actions. */
|
|
74
|
+
| { type: "init"; sprite: number; timeline: number | null; frame: number }
|
|
75
|
+
/**
|
|
76
|
+
* A clip action of the PlaceObject 2 to 4 at `depth`. `events` are those
|
|
77
|
+
* both its flags and the place's field of all events have, the ones Flash
|
|
78
|
+
* runs it for; `key` is keyPress's key code, else 0.
|
|
79
|
+
*/
|
|
80
|
+
| {
|
|
81
|
+
type: "clip";
|
|
82
|
+
timeline: number | null;
|
|
83
|
+
frame: number;
|
|
84
|
+
depth: number;
|
|
85
|
+
events: number;
|
|
86
|
+
key: number;
|
|
87
|
+
}
|
|
88
|
+
/** A button's actions; DefineButton's one runs on overDownToOverUp. `key` is 0 without one. */
|
|
89
|
+
| { type: "button"; button: number; conditions: number; key: number };
|
|
90
|
+
|
|
91
|
+
/** A block of actions: its bytes are `length` from `offset` in the SWF's decompressed bytes. */
|
|
92
|
+
export interface ActionBlock {
|
|
93
|
+
/** Its place in the SWF's list, which the JIT and AOT modules both number blocks by. */
|
|
94
|
+
index: number;
|
|
95
|
+
owner: ActionOwner;
|
|
96
|
+
/** The offset of the tag's body that holds it, the key of `ActionList.byTag`. */
|
|
97
|
+
tag: number;
|
|
98
|
+
offset: number;
|
|
99
|
+
length: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ActionList {
|
|
103
|
+
/** The SWF's version, which decides how its actions run and how its clip events are read. */
|
|
104
|
+
version: number;
|
|
105
|
+
/** In tag order, a sprite's within its DefineSprite, and a tag's in record order. */
|
|
106
|
+
blocks: ActionBlock[];
|
|
107
|
+
/** The indices of each tag's blocks, by the offset of the tag's body. */
|
|
108
|
+
byTag: Map<number, number[]>;
|
|
109
|
+
/**
|
|
110
|
+
* The offsets of the PlaceObject 2 to 4 tags' bodies whose clip actions
|
|
111
|
+
* run past the tag, which have no blocks: Flash reads a place's records
|
|
112
|
+
* as the first of its events comes, and stops playing the whole SWF,
|
|
113
|
+
* every timeline of it, there.
|
|
114
|
+
*/
|
|
115
|
+
corrupt: number[];
|
|
116
|
+
/** The events of each of those tags' field of all events, by its offset: when Flash stops. */
|
|
117
|
+
corruptEvents: Map<number, number>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A SWF's action blocks, a pure function of its bytes. A block may be empty.
|
|
122
|
+
* They are listed in an ActionScript 3 SWF too, where Flash runs none.
|
|
123
|
+
*/
|
|
124
|
+
export function readActions(swf: Swf): ActionList {
|
|
125
|
+
const list: ActionList = {
|
|
126
|
+
version: swf.header.version,
|
|
127
|
+
blocks: [],
|
|
128
|
+
byTag: new Map(),
|
|
129
|
+
corrupt: [],
|
|
130
|
+
corruptEvents: new Map(),
|
|
131
|
+
};
|
|
132
|
+
walk(swf.bytes, swf.tags, null, list);
|
|
133
|
+
return list;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function walk(bytes: Uint8Array, tags: Tag[], timeline: number | null, list: ActionList): void {
|
|
137
|
+
let frame = 0;
|
|
138
|
+
|
|
139
|
+
for (const tag of tags) {
|
|
140
|
+
const end = tag.offset + tag.length;
|
|
141
|
+
switch (tag.code) {
|
|
142
|
+
case ShowFrame:
|
|
143
|
+
frame++;
|
|
144
|
+
break;
|
|
145
|
+
case DoAction:
|
|
146
|
+
add(list, tag, { type: "frame", timeline, frame }, tag.offset, end);
|
|
147
|
+
break;
|
|
148
|
+
case DoInitAction:
|
|
149
|
+
// A tag too short for its sprite id is no block, as Ruffle fails to read it.
|
|
150
|
+
if (tag.length >= 2) {
|
|
151
|
+
const sprite = bytes[tag.offset] | (bytes[tag.offset + 1] << 8);
|
|
152
|
+
add(list, tag, { type: "init", sprite, timeline, frame }, tag.offset + 2, end);
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case PlaceObject2:
|
|
156
|
+
case PlaceObject3:
|
|
157
|
+
case PlaceObject4: {
|
|
158
|
+
const { depth, offset } = clipActionsOffset(bytes, tag);
|
|
159
|
+
if (offset >= 0) {
|
|
160
|
+
clipActions(bytes, tag, offset, list, timeline, frame, depth);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case DefineButton:
|
|
165
|
+
case DefineButton2:
|
|
166
|
+
buttonActions(bytes, tag, list);
|
|
167
|
+
break;
|
|
168
|
+
case DefineSprite: {
|
|
169
|
+
const sprite = readSprite(bytes, tag);
|
|
170
|
+
walk(bytes, sprite.tags, sprite.id, list);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function add(list: ActionList, tag: Tag, owner: ActionOwner, offset: number, end: number): void {
|
|
178
|
+
const index = list.blocks.length;
|
|
179
|
+
list.blocks.push({ index, owner, tag: tag.offset, offset, length: end - offset });
|
|
180
|
+
|
|
181
|
+
const indices = list.byTag.get(tag.offset);
|
|
182
|
+
if (indices) {
|
|
183
|
+
indices.push(index);
|
|
184
|
+
} else {
|
|
185
|
+
list.byTag.set(tag.offset, [index]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function corrupt(list: ActionList, tag: Tag, events: number): void {
|
|
190
|
+
list.corrupt.push(tag.offset);
|
|
191
|
+
list.corruptEvents.set(tag.offset, events);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** CLIPACTIONS: a reserved u16, the events Flash dispatches, then records to one of no events. */
|
|
195
|
+
function clipActions(
|
|
196
|
+
bytes: Uint8Array,
|
|
197
|
+
tag: Tag,
|
|
198
|
+
start: number,
|
|
199
|
+
list: ActionList,
|
|
200
|
+
timeline: number | null,
|
|
201
|
+
frame: number,
|
|
202
|
+
depth: number,
|
|
203
|
+
): void {
|
|
204
|
+
const end = tag.offset + tag.length;
|
|
205
|
+
const r = new SwfReader(bytes, start, end);
|
|
206
|
+
const wide = list.version >= 6;
|
|
207
|
+
const flagSize = wide ? 4 : 2;
|
|
208
|
+
r.u16();
|
|
209
|
+
// Flags cut short end the list whatever their bytes, in adl as in Ruffle (its #2899).
|
|
210
|
+
const flags = () => {
|
|
211
|
+
if (r.pos + flagSize > end) {
|
|
212
|
+
r.pos = end;
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return wide ? r.u32() : r.u16();
|
|
217
|
+
};
|
|
218
|
+
// Flash runs a record only for the events this field also has.
|
|
219
|
+
const all = flags() & CLIP_EVENTS;
|
|
220
|
+
const records: { owner: ActionOwner; start: number; end: number }[] = [];
|
|
221
|
+
|
|
222
|
+
for (;;) {
|
|
223
|
+
const raw = flags();
|
|
224
|
+
if (raw === 0) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A record cut short stops the whole SWF in Flash, before any of the place's records runs.
|
|
229
|
+
if (r.pos + 4 > end) {
|
|
230
|
+
corrupt(list, tag, all);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const events = raw & all;
|
|
235
|
+
const length = r.u32();
|
|
236
|
+
let start = r.pos;
|
|
237
|
+
let key = 0;
|
|
238
|
+
// keyPress's key code is the first byte of the record's length. Flash
|
|
239
|
+
// reads it even of a length of 0, as the next record's first byte, and
|
|
240
|
+
// goes on from there: that record keeps its key and has no actions.
|
|
241
|
+
if (raw & ClipEvent.keyPress) {
|
|
242
|
+
if (r.pos >= end) {
|
|
243
|
+
corrupt(list, tag, all);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
key = bytes[r.pos];
|
|
248
|
+
start += Math.min(length, 1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (length > end - r.pos) {
|
|
252
|
+
corrupt(list, tag, all);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (events !== 0) {
|
|
257
|
+
const owner: ActionOwner = { type: "clip", timeline, frame, depth, events, key };
|
|
258
|
+
records.push({ owner, start, end: r.pos + length });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
r.pos += length;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const record of records) {
|
|
265
|
+
add(list, tag, record.owner, record.start, record.end);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* DefineButton's actions run to the tag's end. DefineButton2's, after its
|
|
271
|
+
* records when its action offset is not 0, are BUTTONCONDACTIONs: a size,
|
|
272
|
+
* 0 for the last, which runs to the end, then the conditions and key.
|
|
273
|
+
*/
|
|
274
|
+
function buttonActions(bytes: Uint8Array, tag: Tag, list: ActionList): void {
|
|
275
|
+
const { id, offset } = buttonActionsOffset(bytes, tag);
|
|
276
|
+
const end = tag.offset + tag.length;
|
|
277
|
+
if (offset < 0) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (tag.code === DefineButton) {
|
|
282
|
+
const owner: ActionOwner = {
|
|
283
|
+
type: "button",
|
|
284
|
+
button: id,
|
|
285
|
+
conditions: ButtonCondition.overDownToOverUp,
|
|
286
|
+
key: 0,
|
|
287
|
+
};
|
|
288
|
+
add(list, tag, owner, offset, end);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const r = new SwfReader(bytes, offset, end);
|
|
293
|
+
for (;;) {
|
|
294
|
+
if (r.pos + 4 > end) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const size = r.u16();
|
|
299
|
+
const conditions = r.u16();
|
|
300
|
+
// Sizes 1 to 3 are shorter than the record's own head: Ruffle's error, so the end here.
|
|
301
|
+
if ((size !== 0 && size < 4) || size - 4 > end - r.pos) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const blockEnd = size === 0 ? end : r.pos + size - 4;
|
|
306
|
+
const owner: ActionOwner = {
|
|
307
|
+
type: "button",
|
|
308
|
+
button: id,
|
|
309
|
+
conditions: conditions & 0x1ff,
|
|
310
|
+
key: conditions >> 9,
|
|
311
|
+
};
|
|
312
|
+
add(list, tag, owner, r.pos, blockEnd);
|
|
313
|
+
if (size === 0) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
r.pos = blockEnd;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// An AIR application's descriptor (`<name>-app.xml`), as adl reads what it
|
|
2
|
+
// needs of one to run it: the application's id, and its first window's
|
|
3
|
+
// content SWF, title and size. Elements are found by name, whatever
|
|
4
|
+
// namespace the descriptor's AIR version gives them; what is not there is
|
|
5
|
+
// left to its default. A desktop app reads it from disk, a page from an
|
|
6
|
+
// AIR package (`.air`) or the files of one.
|
|
7
|
+
|
|
8
|
+
export interface AirDescriptor {
|
|
9
|
+
id: string;
|
|
10
|
+
/** The content SWF's path, relative to the application's directory, "/"-separated. */
|
|
11
|
+
content: string;
|
|
12
|
+
title: string | null;
|
|
13
|
+
width: number | null;
|
|
14
|
+
height: number | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The text of the first `<name>` element in `xml`, entities decoded; null for none. */
|
|
18
|
+
function element(xml: string, name: string): string | null {
|
|
19
|
+
const m = new RegExp(
|
|
20
|
+
`<(?:[\\w.-]+:)?${name}(?:\\s[^>]*)?>([\\s\\S]*?)</(?:[\\w.-]+:)?${name}\\s*>`,
|
|
21
|
+
).exec(xml);
|
|
22
|
+
return m ? decode(m[1].trim()) : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** XML's five entities and its character references, and a CDATA section's text. */
|
|
26
|
+
function decode(text: string): string {
|
|
27
|
+
const cdata = /^<!\[CDATA\[([\s\S]*)\]\]>$/.exec(text);
|
|
28
|
+
if (cdata) {
|
|
29
|
+
return cdata[1];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (_, e: string) => {
|
|
33
|
+
switch (e.toLowerCase()) {
|
|
34
|
+
case "amp":
|
|
35
|
+
return "&";
|
|
36
|
+
case "lt":
|
|
37
|
+
return "<";
|
|
38
|
+
case "gt":
|
|
39
|
+
return ">";
|
|
40
|
+
case "quot":
|
|
41
|
+
return '"';
|
|
42
|
+
case "apos":
|
|
43
|
+
return "'";
|
|
44
|
+
default:
|
|
45
|
+
return String.fromCodePoint(
|
|
46
|
+
e[1].toLowerCase() === "x" ? Number.parseInt(e.slice(2), 16) : Number(e.slice(1)),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A whole number of pixels, or null for none or nonsense. */
|
|
53
|
+
function size(text: string | null): number | null {
|
|
54
|
+
const n = text === null ? Number.NaN : Number(text);
|
|
55
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What `xml`, a descriptor, says; an Error naming what it lacks that adl requires. */
|
|
59
|
+
export function readAirDescriptor(xml: string): AirDescriptor {
|
|
60
|
+
if (!/<(?:[\w.-]+:)?application[\s>]/.test(xml)) {
|
|
61
|
+
throw new Error("not an AIR application descriptor: no <application>");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The comments out first, which may hold an element of each name.
|
|
65
|
+
const text = xml.replace(/<!--[\s\S]*?-->/g, "");
|
|
66
|
+
const id = element(text, "id");
|
|
67
|
+
const window = element(text, "initialWindow");
|
|
68
|
+
const content = window === null ? null : element(window, "content");
|
|
69
|
+
if (!id) {
|
|
70
|
+
throw new Error("the descriptor names no <id>");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!content) {
|
|
74
|
+
throw new Error("the descriptor's <initialWindow> names no <content>");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id,
|
|
79
|
+
content: content.replaceAll("\\", "/"),
|
|
80
|
+
title: window === null ? null : element(window, "title"),
|
|
81
|
+
width: window === null ? null : size(element(window, "width")),
|
|
82
|
+
height: window === null ? null : size(element(window, "height")),
|
|
83
|
+
};
|
|
84
|
+
}
|