@swf2es/runtime 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/avm2/cache-fill.d.ts +4 -1
  2. package/dist/avm2/cache-fill.d.ts.map +1 -1
  3. package/dist/avm2/cache-fill.js +5 -6
  4. package/dist/avm2/cache-fill.js.map +1 -1
  5. package/dist/avm2/descriptors.d.ts +6 -1
  6. package/dist/avm2/descriptors.d.ts.map +1 -1
  7. package/dist/avm2/descriptors.js.map +1 -1
  8. package/dist/avm2/errors.d.ts +14 -0
  9. package/dist/avm2/errors.d.ts.map +1 -1
  10. package/dist/avm2/errors.js +17 -0
  11. package/dist/avm2/errors.js.map +1 -1
  12. package/dist/avm2/index.d.ts +1 -0
  13. package/dist/avm2/index.d.ts.map +1 -1
  14. package/dist/avm2/index.js +1 -0
  15. package/dist/avm2/index.js.map +1 -1
  16. package/dist/avm2/names.d.ts +11 -0
  17. package/dist/avm2/names.d.ts.map +1 -1
  18. package/dist/avm2/names.js +12 -0
  19. package/dist/avm2/names.js.map +1 -1
  20. package/dist/avm2/natives/proxy.d.ts.map +1 -1
  21. package/dist/avm2/natives/proxy.js +9 -4
  22. package/dist/avm2/natives/proxy.js.map +1 -1
  23. package/dist/avm2/options.d.ts +5 -0
  24. package/dist/avm2/options.d.ts.map +1 -1
  25. package/dist/avm2/options.js.map +1 -1
  26. package/dist/avm2/property-cache.d.ts +17 -13
  27. package/dist/avm2/property-cache.d.ts.map +1 -1
  28. package/dist/avm2/property-cache.js +50 -10
  29. package/dist/avm2/property-cache.js.map +1 -1
  30. package/dist/avm2/runtime.d.ts +62 -0
  31. package/dist/avm2/runtime.d.ts.map +1 -1
  32. package/dist/avm2/runtime.js +245 -25
  33. package/dist/avm2/runtime.js.map +1 -1
  34. package/dist/avm2/traits.d.ts +7 -0
  35. package/dist/avm2/traits.d.ts.map +1 -1
  36. package/dist/avm2/traits.js +6 -0
  37. package/dist/avm2/traits.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/avm2/cache-fill.ts +5 -7
  40. package/src/avm2/descriptors.ts +6 -2
  41. package/src/avm2/errors.ts +18 -0
  42. package/src/avm2/index.ts +1 -0
  43. package/src/avm2/names.ts +13 -0
  44. package/src/avm2/natives/proxy.ts +20 -4
  45. package/src/avm2/options.ts +5 -0
  46. package/src/avm2/property-cache.ts +57 -11
  47. package/src/avm2/runtime.ts +280 -26
  48. package/src/avm2/traits.ts +7 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swf2es/runtime",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "AVM2 and AVM1 language runtime used by generated code",
5
5
  "keywords": [
6
6
  "actionscript",
@@ -41,6 +41,6 @@
41
41
  "LICENSE.third-party"
42
42
  ],
43
43
  "dependencies": {
44
- "@swf2es/format": "0.1.14"
44
+ "@swf2es/format": "0.1.16"
45
45
  }
46
46
  }
@@ -33,7 +33,10 @@ import {
33
33
  /**
34
34
  * Keep what `mn` found on `traits`, `o`'s, in `head`, its cache: a
35
35
  * binding, as getProperty, setProperty and callProperty use it, or a
36
- * dynamic property's name. Not where the name resolves otherwise: through
36
+ * dynamic property's name, on a sealed object too, whose prototypes have
37
+ * dynamic properties all the same (a script may give a class's prototype a
38
+ * function, and call it on the class's objects by name, as an obfuscator
39
+ * may have it do). Not where the name resolves otherwise: through
37
40
  * a property hook (a Proxy's, XML's), an index (a Vector's, an Array's
38
41
  * element), or a Dictionary's key; nor on a primitive's dynamic property,
39
42
  * which its prototypes have.
@@ -58,7 +61,6 @@ export function fillCache(
58
61
  name === null ||
59
62
  (typeof o !== "object" && typeof o !== "function") ||
60
63
  hook !== null ||
61
- !traits.dynamic ||
62
64
  traits.refusesNames ||
63
65
  traits.getIndex !== undefined ||
64
66
  traits.index !== undefined ||
@@ -109,11 +111,7 @@ export function fillCache(
109
111
  mn.cache = head;
110
112
  }
111
113
 
112
- const e = entryFor(head, traits);
113
- if (e === null) {
114
- return;
115
- }
116
-
114
+ const e = entryFor(head, traits, mn);
117
115
  e.traits = traits;
118
116
  e.kind = kind;
119
117
  e.key = key;
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { Domain, Script } from "./domain.js";
6
6
  import type { Multiname, Namespace, TypeName } from "./names.js";
7
+ import type { ScopeFinds } from "./property-cache.js";
7
8
  import type { ClassRef, VectorRef } from "./traits.js";
8
9
 
9
10
  // biome-ignore lint/suspicious/noExplicitAny: AS3 values are untyped
@@ -16,8 +17,11 @@ export type Method = Function;
16
17
  /** Not a property: distinct from undefined, which a property can hold. */
17
18
  export const NOT_FOUND = Symbol("not found");
18
19
 
19
- /** A scope chain: its objects, outermost first, and a bit per with scope in w. */
20
- export type Scope = Value[] & { w: number };
20
+ /**
21
+ * A scope chain: its objects, outermost first, and a bit per with scope in
22
+ * w; what findProperty found on it, once asked (Runtime.outerScopeOf).
23
+ */
24
+ export type Scope = Value[] & { w: number; found?: ScopeFinds | null };
21
25
 
22
26
  /** A method factory: the method's function, once its scope chain is known. */
23
27
  export type Factory = (scope: Scope, sup: AsObject | null) => Method;
@@ -11,6 +11,14 @@ import type { Runtime } from "./runtime.js";
11
11
  /** Thrown for an AS3 exception that is an Error the runtime made, before its class existed. */
12
12
  export class AsError extends Error {}
13
13
 
14
+ /**
15
+ * Thrown where a SWF asks for something swf2es has not got (Runtime.unsupported).
16
+ * Uncaught, it stops the script as any error of the runtime's own does; an
17
+ * AS3 catch gets it as an AS3 error (Runtime.caught), since content written
18
+ * for several runtimes guards what one of them may lack with try and catch.
19
+ */
20
+ export class UnsupportedError extends Error {}
21
+
14
22
  /**
15
23
  * Where an AS3 error was made, as JavaScript stack lines from the first
16
24
  * compiled method's (their names begin with `$`), the runtime's and the
@@ -29,6 +37,16 @@ export function stackOf(error: Value): string | null {
29
37
  return frames.slice(Math.max(0, first)).join("\n");
30
38
  }
31
39
 
40
+ /**
41
+ * What an error the runtime made says in full, its text and arguments as
42
+ * debugger mode words them, for a host to show: the release player's
43
+ * message, which the SWF sees, is the number alone. Null for any other value.
44
+ */
45
+ export function detailOf(error: Value): string | null {
46
+ const detail = (error as { $detail?: unknown } | null)?.$detail;
47
+ return typeof detail === "string" ? detail : null;
48
+ }
49
+
32
50
  /**
33
51
  * Error `id`'s message as AS3 sees it: its number, as the release player
34
52
  * and avmshell give it, or with its text and arguments in debugger mode.
package/src/avm2/index.ts CHANGED
@@ -8,6 +8,7 @@ export { Reader as Amf3Reader, Writer as Amf3Writer } from "./amf.js";
8
8
  export { builtinLibrary } from "./builtin-library.js";
9
9
  export type { Abc, AsObject, CompileUnit, FoundDefinition, Method, Value } from "./descriptors.js";
10
10
  export { type Domain, frameScripts, frameSites, siteScript, stackFrames } from "./domain.js";
11
+ export { UnsupportedError } from "./errors.js";
11
12
  export { FloatValue, hasFloatLibrary } from "./float.js";
12
13
  export { floatLibrary } from "./float-library.js";
13
14
  export type { ClassHook, NativesProvider } from "./hooks.js";
package/src/avm2/names.ts CHANGED
@@ -85,6 +85,11 @@ export const CONSTANT_MultinameA = 0x0e;
85
85
  export const CONSTANT_MultinameL = 0x1b;
86
86
  export const CONSTANT_MultinameLA = 0x1c;
87
87
 
88
+ // What a name made at run time was made of (Multiname.made).
89
+ export const MADE_Unknown = 0;
90
+ export const MADE_Number = 1;
91
+ export const MADE_Value = 2;
92
+
88
93
  /**
89
94
  * A name as a property lookup uses it: its namespaces, each with the API
90
95
  * version it sees bindings at, and its local name, null for any name.
@@ -121,6 +126,14 @@ export class Multiname {
121
126
  cls: AsObject | null = null;
122
127
  /** Fixed at construction so indexed access needs no getter in its hot path. */
123
128
  readonly elementName: boolean;
129
+ /**
130
+ * What made it, where the code's own value did at run time: a number
131
+ * read or written as an element (MADE_Number), which avmplus hands a
132
+ * Proxy as its string, or a string or any other value (MADE_Value), which
133
+ * it hands it as a QName. MADE_Unknown for a name of the code's own and
134
+ * one the runtime made.
135
+ */
136
+ made = MADE_Unknown;
124
137
 
125
138
  constructor(
126
139
  readonly kind: number,
@@ -7,7 +7,16 @@
7
7
 
8
8
  import type { AsObject, Value } from "../descriptors.js";
9
9
  import type { ClassHook, PropertyHook } from "../hooks.js";
10
- import { type Multiname, NS_Public, namespace, publicNs, publicQname, qname } from "../names.js";
10
+ import {
11
+ MADE_Number,
12
+ MADE_Unknown,
13
+ type Multiname,
14
+ NS_Public,
15
+ namespace,
16
+ publicNs,
17
+ publicQname,
18
+ qname,
19
+ } from "../names.js";
11
20
  import type { Runtime } from "../runtime.js";
12
21
  import type { Natives } from "./define.js";
13
22
 
@@ -15,8 +24,11 @@ const FLASH_PROXY = namespace(NS_Public, "http://www.adobe.com/2006/actionscript
15
24
 
16
25
  /**
17
26
  * The name a flash_proxy method gets for `mn`: a QName that was the name
18
- * stays one, an index comes as its string, as avmplus' getUintProperty
19
- * interns it, and any other name as a QName in its namespace.
27
+ * stays one; a number read or written as an element comes as its string,
28
+ * as avmplus' getUintProperty and getpropertylate_d intern it, whatever
29
+ * the number; a string or another value of the code's comes as a QName,
30
+ * "0" too, as does any other name, in its namespace. A name whose maker
31
+ * is not known is told by its string: an index's is the string.
20
32
  */
21
33
  function nameFor(rt: Runtime, mn: Multiname): Value {
22
34
  if (mn.key !== undefined && (mn.key as AsObject)?.$local !== undefined) {
@@ -24,7 +36,11 @@ function nameFor(rt: Runtime, mn: Multiname): Value {
24
36
  }
25
37
 
26
38
  const name = mn.name;
27
- if (name !== null && !mn.attribute && isIndex(name)) {
39
+ if (
40
+ name !== null &&
41
+ !mn.attribute &&
42
+ (mn.made === MADE_Number || (mn.made === MADE_Unknown && isIndex(name)))
43
+ ) {
28
44
  return name;
29
45
  }
30
46
 
@@ -8,6 +8,11 @@ import type { Runtime } from "./runtime.js";
8
8
  export interface RuntimeOptions {
9
9
  /** Where trace and print write a line. */
10
10
  print?: (line: string) => void;
11
+ /**
12
+ * Told what is not supported that the code asked for and caught, once
13
+ * for each: caught, it reaches no one else.
14
+ */
15
+ onUnsupported?: (message: string) => void;
11
16
  /**
12
17
  * Behave as the debugger player: error messages carry avmplus' text
13
18
  * ("Error #1009: Cannot access ..."), and System.isDebugger is true. By
@@ -11,8 +11,13 @@
11
11
  // entry still looks for the property on the object, which may have lost it,
12
12
  // then on its prototypes, which may have gained or lost it, and leaves
13
13
  // what neither has to the full lookup.
14
+ //
15
+ // A name looked up on more traits than its cache keeps, as `x` is on every
16
+ // class with an x, stops replacing entries; what it finds on traits it has
17
+ // no entry for is kept on those traits instead, by name, so that the one
18
+ // class it is mostly read on is not left to the full lookup.
14
19
 
15
- import type { Method, TypeRef } from "./descriptors.js";
20
+ import type { AsObject, Method, TypeRef } from "./descriptors.js";
16
21
  import type { Multiname } from "./names.js";
17
22
  import type { Traits } from "./traits.js";
18
23
 
@@ -25,7 +30,7 @@ export const IC_GetSet = 5;
25
30
  export const IC_Method = 6;
26
31
  /** A method a property hook resolves the name of for get and set, as XML's: callproperty only. */
27
32
  export const IC_Call = 7;
28
- /** No binding: a dynamic property, the object's own or else a prototype's, if either has it. */
33
+ /** No binding: a dynamic property, the object's own (a dynamic object's) or else a prototype's, if either has it. */
29
34
  export const IC_Dynamic = 8;
30
35
 
31
36
  /** Entries a name keeps, for as many traits, before it replaces the oldest. */
@@ -35,7 +40,7 @@ const ENTRIES = 8;
35
40
  * keeps would replace one on every miss, and still miss as often. It keeps
36
41
  * those it has until the epoch changes.
37
42
  */
38
- export const REPLACEMENTS = 16;
43
+ const REPLACEMENTS = 16;
39
44
 
40
45
  /**
41
46
  * One traits' entry for a name. A name's first entry heads its list and
@@ -70,6 +75,8 @@ export function epoch(): number {
70
75
 
71
76
  /** The caches filled in this epoch, which invalidate empties so that they keep no traits or code. */
72
77
  const filled: PropertyCache[] = [];
78
+ /** The traits given entries of their own in this epoch (Traits.manyTraitsEntries). */
79
+ const filledTraits: Traits[] = [];
73
80
 
74
81
  /** Note that `head`'s cache is being filled in this epoch. */
75
82
  function filling(head: PropertyCache): void {
@@ -88,6 +95,11 @@ export function invalidate(): void {
88
95
  }
89
96
 
90
97
  filled.length = 0;
98
+ for (let i = 0; i < filledTraits.length; i++) {
99
+ filledTraits[i].manyTraitsEntries = null;
100
+ }
101
+
102
+ filledTraits.length = 0;
91
103
  current++;
92
104
  }
93
105
 
@@ -98,11 +110,12 @@ export const UNFILLED = new PropertyCache();
98
110
 
99
111
  /** The entry of `mn`'s cache for objects with traits `t`, or null. */
100
112
  export function cached(t: Traits | undefined, mn: Multiname): PropertyCache | null {
101
- let e: PropertyCache | null = mn.cache;
102
- if (t === undefined || e.epoch !== epoch()) {
113
+ const head = mn.cache;
114
+ if (t === undefined || head.epoch !== epoch()) {
103
115
  return null;
104
116
  }
105
117
 
118
+ let e: PropertyCache | null = head;
106
119
  do {
107
120
  if (e.traits === t) {
108
121
  return e;
@@ -111,16 +124,20 @@ export function cached(t: Traits | undefined, mn: Multiname): PropertyCache | nu
111
124
  e = e.next;
112
125
  } while (e !== null);
113
126
 
127
+ if (head.replaced === REPLACEMENTS && t.manyTraitsEntries !== null) {
128
+ return t.manyTraitsEntries.get(mn) ?? null;
129
+ }
130
+
114
131
  return null;
115
132
  }
116
133
 
117
134
  /**
118
- * The entry of a cache to fill for traits `t`: its own, a free one, a new
119
- * one while the list is shorter than ENTRIES, else the oldest, until it has
120
- * replaced REPLACEMENTS; then none. A cache of an earlier epoch is emptied
121
- * first.
135
+ * The entry of `mn`'s cache, `head`, to fill for traits `t`: its own, a
136
+ * free one, a new one while the list is shorter than ENTRIES, else the
137
+ * oldest, until it has replaced REPLACEMENTS; then one `t` keeps for `mn`.
138
+ * A cache of an earlier epoch is emptied first.
122
139
  */
123
- export function entryFor(head: PropertyCache, t: Traits): PropertyCache | null {
140
+ export function entryFor(head: PropertyCache, t: Traits, mn: Multiname): PropertyCache {
124
141
  if (head.epoch !== epoch()) {
125
142
  for (let e: PropertyCache | null = head; e !== null; e = e.next) {
126
143
  e.traits = null;
@@ -150,7 +167,7 @@ export function entryFor(head: PropertyCache, t: Traits): PropertyCache | null {
150
167
  }
151
168
 
152
169
  if (head.replaced === REPLACEMENTS) {
153
- return null;
170
+ return traitsEntry(t, mn);
154
171
  }
155
172
 
156
173
  head.replaced++;
@@ -158,3 +175,32 @@ export function entryFor(head: PropertyCache, t: Traits): PropertyCache | null {
158
175
  head.victim = victim.next ?? head;
159
176
  return victim;
160
177
  }
178
+
179
+ /** The entry `t` keeps for `mn`, a name its cache has no room for. */
180
+ function traitsEntry(t: Traits, mn: Multiname): PropertyCache {
181
+ let entries = t.manyTraitsEntries;
182
+ if (entries === null) {
183
+ entries = new Map();
184
+ t.manyTraitsEntries = entries;
185
+ filledTraits.push(t);
186
+ }
187
+
188
+ let e = entries.get(mn);
189
+ if (e === undefined) {
190
+ e = new PropertyCache();
191
+ entries.set(mn, e);
192
+ }
193
+
194
+ return e;
195
+ }
196
+
197
+ /**
198
+ * Which of a scope chain's objects binds each name findProperty looked up
199
+ * on it, the innermost that does, or null where none does, in the epoch it
200
+ * was found in: a chain's objects never change, nor do their traits'
201
+ * bindings within an epoch. Only for a chain without a with scope, which
202
+ * has a name by its dynamic properties too.
203
+ */
204
+ export class ScopeFinds extends Map<Multiname, AsObject | null> {
205
+ epoch = current;
206
+ }