buninu-browser 0.2.1 → 0.2.3

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 (90) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +74 -9
  3. package/ai-debug.log +5537 -0
  4. package/bunfig.toml +1 -0
  5. package/buninu-browser.js +4 -0
  6. package/index.js +5 -0
  7. package/lib/audio/piano.js +24 -1
  8. package/lib/audio/playback-buffer.js +27 -0
  9. package/lib/audio/pulse-client.js +137 -19
  10. package/lib/cdp-server.js +22 -11
  11. package/lib/happy-dom/parser.js +29 -4
  12. package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
  13. package/lib/headless-shell.js +69 -19
  14. package/lib/input/hit-test.js +25 -7
  15. package/lib/layout/text-layout.js +268 -85
  16. package/lib/log.js +5 -2
  17. package/lib/media/byte-pipe.js +82 -0
  18. package/lib/media/position-audio.js +42 -0
  19. package/lib/media/video-keyframe.js +29 -0
  20. package/lib/network/network-service.js +161 -14
  21. package/lib/paint/display-list.js +316 -15
  22. package/lib/paint/gradient.js +11 -5
  23. package/lib/render-tree/index.js +19 -2
  24. package/lib/render-tree/svg.js +2 -1
  25. package/lib/renderer/bindings.js +995 -69
  26. package/lib/renderer/canvas-2d.js +92 -0
  27. package/lib/renderer/compositor.js +16 -1
  28. package/lib/renderer/custom-element-reactions.js +18 -0
  29. package/lib/renderer/history.js +42 -0
  30. package/lib/renderer/host.js +166 -9
  31. package/lib/renderer/image-loading.js +27 -0
  32. package/lib/renderer/interfaces.js +215 -0
  33. package/lib/renderer/keyboard-input.js +12 -0
  34. package/lib/renderer/main.js +162 -12
  35. package/lib/renderer/media-playback-events.js +13 -0
  36. package/lib/renderer/media-source.js +819 -0
  37. package/lib/renderer/messaging.js +411 -0
  38. package/lib/renderer/native-functions.js +113 -0
  39. package/lib/renderer/page-realm.js +1408 -242
  40. package/lib/renderer/page-renderer.js +1893 -223
  41. package/lib/renderer/piano-input.js +22 -0
  42. package/lib/renderer/pointer-move.js +29 -0
  43. package/lib/renderer/rejections.js +31 -0
  44. package/lib/renderer/rewrite-worker.js +22 -0
  45. package/lib/renderer/script-rewrite.js +805 -57
  46. package/lib/renderer/trusted-types.js +129 -0
  47. package/lib/renderer/video-presentation.js +126 -0
  48. package/lib/renderer/workers.js +327 -0
  49. package/lib/screenshot-cli.js +2 -0
  50. package/lib/style/computed-style.js +186 -29
  51. package/lib/user-agent.js +14 -2
  52. package/package.json +4 -1
  53. package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
  54. package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
  55. package/patches/happy-dom/README.md +26 -0
  56. package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
  57. package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
  58. package/patches/ses/README.md +4 -0
  59. package/test/canvas-2d.test.js +55 -0
  60. package/test/compositor.test.js +41 -1
  61. package/test/computed-style.test.js +59 -1
  62. package/test/display-list.test.js +88 -1
  63. package/test/download.test.js +43 -0
  64. package/test/fixtures/long-video.mp4 +0 -0
  65. package/test/fixtures/media-sync.js +57 -0
  66. package/test/happy-dom-platform-adapters.test.js +25 -0
  67. package/test/headless-shell.test.js +23 -1
  68. package/test/hit-test.test.js +25 -1
  69. package/test/media-playback-events.test.js +22 -0
  70. package/test/media-source.test.js +156 -0
  71. package/test/media-sync.test.js +12 -0
  72. package/test/mobile.test.js +2 -2
  73. package/test/navigation.test.js +61 -0
  74. package/test/network-service.test.js +14 -0
  75. package/test/page-scripts.test.js +1203 -7
  76. package/test/piano-input.test.js +44 -0
  77. package/test/playback-buffer.test.js +31 -0
  78. package/test/position-audio.test.js +67 -0
  79. package/test/positioning.test.js +95 -1
  80. package/test/pulse-client.test.js +84 -0
  81. package/test/renderer-host.test.js +5 -2
  82. package/test/screenshot-cli.test.js +25 -1
  83. package/test/script-rewrite.test.js +111 -6
  84. package/test/setup.js +5 -0
  85. package/test/text-layout.test.js +62 -0
  86. package/test/user-agent.test.js +60 -4
  87. package/test/video-presentation.test.js +67 -0
  88. package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
  89. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
  90. package/vendor/ses/dist/ses.mjs +52 -6
@@ -15,32 +15,116 @@
15
15
  *
16
16
  * Declarations inside functions are untouched. Strict-mode-only restrictions
17
17
  * (`with`, legacy octal literals, `arguments.callee`) cannot be lifted.
18
+ *
19
+ * A sloppy-mode function called without a receiver gets the global object
20
+ * as `this`: `(function () { return this; })()` and `Function("return this")()`
21
+ * are how many libraries find it. The engine's realm global is not the
22
+ * page's, and in strict code `this` is undefined there, so in functions of
23
+ * sloppy code (not arrow functions, which take `this` from outside, nor
24
+ * classes or "use strict" code) `this` becomes `$buninu$self(this)` (see
25
+ * THIS_REWRITE): the page global for undefined, null or the realm global.
26
+ *
27
+ * The result also says whether the source parsed (only parsed, rewritten
28
+ * code may run as sloppy-mode code; see page-realm) and whether the program
29
+ * itself is strict, which the prologue would otherwise hide.
18
30
  */
19
31
  import { Parser } from "../../vendor/acorn/dist/acorn.mjs";
20
32
 
21
33
  const FUNCTION_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
22
34
 
35
+ /**
36
+ * Calls `callback(child, key)` for each child node of an Acorn node. A
37
+ * for-in over the node's own keys (Acorn nodes are plain objects) instead of
38
+ * Object.entries: the rewrites walk every node of every script, and
39
+ * BotGuard-style pages evaluate hundreds of sources at run time.
40
+ */
41
+ function forEachChild(node, callback) {
42
+ for (const key in node) {
43
+ if (key === "type" || key === "start" || key === "end") continue;
44
+ const value = node[key];
45
+ if (value === null || typeof value !== "object") continue;
46
+ if (Array.isArray(value)) {
47
+ for (const child of value) if (child !== null && typeof child?.type === "string") callback(child, key);
48
+ } else if (typeof value.type === "string") {
49
+ callback(value, key);
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Applies non-overlapping edits ({ start, end, text }), listed in the order
56
+ * they would be applied from the end of the source, in one pass instead of
57
+ * re-slicing the whole source per edit. At one position the edit listed
58
+ * later ends up first, as when applying them one by one from the end.
59
+ */
60
+ function applyEdits(source, edits) {
61
+ const pieces = [];
62
+ let at = 0;
63
+ for (let index = edits.length - 1; index >= 0; index--) {
64
+ const { start, end, text } = edits[index];
65
+ if (start < at) {
66
+ // Overlapping edits: apply them one by one, from the end.
67
+ let code = source;
68
+ for (const edit of edits) code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
69
+ return code;
70
+ }
71
+ pieces.push(source.slice(at, start), text);
72
+ at = end;
73
+ }
74
+ pieces.push(source.slice(at));
75
+ return pieces.join("");
76
+ }
77
+
78
+ /**
79
+ * Parses a script or module with the options every rewrite uses, or returns
80
+ * `program` when a previous rewrite already parsed this exact source (each
81
+ * rewrite that leaves its source unchanged hands its AST on; parsing a large
82
+ * bundle costs about as much as the engine's own parse).
83
+ */
84
+ function parseSource(source, sourceType, program = null) {
85
+ return program ?? Parser.parse(source, { ecmaVersion: "latest", sourceType, allowHashBang: true });
86
+ }
87
+
88
+ /** The page global function that a sloppy-mode `this` goes through (see THIS_REWRITE). */
89
+ export const THIS_HELPER = "$buninu$self";
90
+ /** A sloppy-mode function's `this` as the rewrite leaves it (restoreSourceText undoes it). */
91
+ export const THIS_REWRITE = `${THIS_HELPER}(this)`;
92
+
23
93
  /**
24
94
  * @param {string} source classic script text
25
- * @returns {{ code: string, globals: string[], rewritten: boolean }}
95
+ * @param {object | null} [parsed] the AST of `source`, when a previous rewrite has it
96
+ * @returns {{ code: string, globals: string[], rewritten: boolean, parsed: boolean, strict: boolean }}
26
97
  */
27
- export function rewriteClassicScript(source) {
98
+ export function rewriteClassicScript(source, parsed = null) {
28
99
  let program;
29
100
  try {
30
- program = Parser.parse(source, {
31
- ecmaVersion: "latest",
32
- sourceType: "script",
33
- allowHashBang: true,
34
- allowReturnOutsideFunction: false,
35
- });
101
+ program = parseSource(source, "script", parsed);
36
102
  } catch {
37
103
  // Let the engine report the syntax error with its own message.
38
- return { code: source, globals: [], rewritten: false };
104
+ return { code: source, globals: [], rewritten: false, parsed: false, strict: false };
39
105
  }
106
+ const strict = hasUseStrict(program);
40
107
 
41
108
  const edits = [];
42
109
  const globals = new Set();
43
110
  const hoisted = [];
111
+ // Edits inside statements: sloppy `this`, and global reads (see globalReferenceEdits).
112
+ const globalEdits = !strict && hasBlockFunctionOutsideFunctions(program) ? [] : globalReferenceEdits(source, program);
113
+ const innerEdits = [
114
+ ...sloppyThisExpressions(program).map((node) => ({ start: node.start, end: node.end, text: THIS_REWRITE })),
115
+ ...globalEdits.map(({ node, text: replacement }) => ({ start: node.start, end: node.end, text: replacement })),
116
+ ].sort((left, right) => left.start - right.start);
117
+ // Source text with the inner edits inside it applied.
118
+ const text = (start, end) => {
119
+ let result = "";
120
+ let at = start;
121
+ for (const edit of innerEdits) {
122
+ if (edit.start < start || edit.end > end) continue;
123
+ result += `${source.slice(at, edit.start)}${edit.text}`;
124
+ at = edit.end;
125
+ }
126
+ return result + source.slice(at, end);
127
+ };
44
128
 
45
129
  const declare = (pattern) => {
46
130
  for (const name of boundNames(pattern)) globals.add(name);
@@ -50,8 +134,8 @@ export function rewriteClassicScript(source) {
50
134
  const declarationAsExpression = (declaration) => declaration.declarations
51
135
  .filter((declarator) => declarator.init)
52
136
  .map((declarator) => {
53
- const target = source.slice(declarator.id.start, declarator.id.end);
54
- const value = source.slice(declarator.init.start, declarator.init.end);
137
+ const target = text(declarator.id.start, declarator.id.end);
138
+ const value = text(declarator.init.start, declarator.init.end);
55
139
  return declarator.id.type === "Identifier" ? `${target} = ${value}` : `(${target} = ${value})`;
56
140
  })
57
141
  .join(", ");
@@ -66,7 +150,7 @@ export function rewriteClassicScript(source) {
66
150
  if (FUNCTION_TYPES.has(node.type)) {
67
151
  if (node.type === "FunctionDeclaration" && programChild) {
68
152
  globals.add(node.id.name);
69
- hoisted.push(`${node.id.name} = ${source.slice(node.start, node.end)};`);
153
+ hoisted.push(`${node.id.name} = ${text(node.start, node.end)};`);
70
154
  edits.push({ start: node.start, end: node.end, text: "" });
71
155
  }
72
156
  return;
@@ -74,62 +158,118 @@ export function rewriteClassicScript(source) {
74
158
  if (node.type === "ClassBody" || node.type === "StaticBlock") return;
75
159
  if (node.type === "VariableDeclaration" && (node.kind === "var" || programChild)) {
76
160
  for (const declarator of node.declarations) declare(declarator.id);
77
- edits.push({ start: node.start, end: node.end, text: variableReplacement(node) });
161
+ edits.push({ start: node.start, end: node.end, text: variableReplacement(node, parent) });
78
162
  return;
79
163
  }
80
164
  if (node.type === "ClassDeclaration" && programChild) {
81
165
  globals.add(node.id.name);
82
- edits.push({ start: node.start, end: node.end, text: `${node.id.name} = ${source.slice(node.start, node.end)};` });
166
+ edits.push({ start: node.start, end: node.end, text: `${node.id.name} = ${text(node.start, node.end)};` });
83
167
  return;
84
168
  }
85
- for (const [key, value] of Object.entries(node)) {
86
- if (key === "type" || key === "start" || key === "end") continue;
87
- for (const child of Array.isArray(value) ? value : [value]) {
88
- if (child && typeof child === "object") visit(child, node);
89
- }
90
- }
169
+ forEachChild(node, (child) => visit(child, node));
91
170
  };
92
171
 
93
172
  /**
94
173
  * The statement a `var` becomes depends on where it sits: a plain
95
174
  * statement, the init of a for loop, or the left side of for-in/of.
96
175
  */
97
- const variableReplacement = (node) => {
98
- const parent = parentOf.get(node);
176
+ const variableReplacement = (node, parent) => {
99
177
  if (parent && (parent.type === "ForInStatement" || parent.type === "ForOfStatement") && parent.left === node) {
100
- return source.slice(node.declarations[0].id.start, node.declarations[0].id.end);
178
+ return text(node.declarations[0].id.start, node.declarations[0].id.end);
101
179
  }
102
180
  const expression = declarationAsExpression(node);
103
181
  if (parent && parent.type === "ForStatement" && parent.init === node) return expression;
104
182
  return expression ? `${expression};` : ";";
105
183
  };
106
184
 
107
- const parentOf = new Map();
108
- (function link(node) {
109
- for (const [key, value] of Object.entries(node)) {
110
- if (key === "type" || key === "start" || key === "end") continue;
111
- const children = Array.isArray(value) ? value : [value];
112
- for (const child of children) {
113
- if (child && typeof child.type === "string") {
114
- parentOf.set(child, node);
115
- link(child);
116
- }
117
- }
118
- }
119
- })(program);
120
185
 
121
186
  visit(program, null);
122
- if (!edits.length) return { code: source, globals: [], rewritten: false };
187
+ // Inner edits outside the statements replaced above (those carry theirs).
188
+ const replaced = [...edits].sort((left, right) => left.start - right.start);
189
+ let next = 0;
190
+ for (const edit of innerEdits) {
191
+ while (next < replaced.length && replaced[next].end <= edit.start) next++;
192
+ const container = replaced[next];
193
+ if (!container || container.start > edit.start || edit.end > container.end) edits.push(edit);
194
+ }
195
+ if (!edits.length) return { code: source, globals: [], rewritten: false, parsed: true, strict };
123
196
 
124
197
  edits.sort((left, right) => right.start - left.start);
125
- let body = source;
126
- for (const edit of edits) body = body.slice(0, edit.start) + edit.text + body.slice(edit.end);
198
+ const body = applyEdits(source, edits);
127
199
  // Hoisting: names exist (as undefined) before any statement runs, and
128
200
  // function declarations are assigned first. Existing globals are kept.
129
201
  const prologue = [...globals]
130
202
  .map((name) => `if (!(${JSON.stringify(name)} in globalThis)) globalThis[${JSON.stringify(name)}] = undefined;`)
131
203
  .join(" ");
132
- return { code: `${prologue} ${hoisted.join(" ")}\n${body}`, globals: [...globals], rewritten: true };
204
+ const binding = globalEdits.length ? `const ${GLOBAL_BINDING} = globalThis; ` : "";
205
+ return { code: `${binding}${prologue} ${hoisted.join(" ")}\n${body}`, globals: [...globals], rewritten: true, parsed: true, strict };
206
+ }
207
+
208
+ /**
209
+ * A classic script as the page realm evaluates it: direct evals, then
210
+ * constructor reads, then the classic-script rewrite, parsing the source
211
+ * once when the earlier rewrites leave it unchanged.
212
+ *
213
+ * @param {string} source
214
+ */
215
+ export function rewriteClassicScriptSource(source) {
216
+ const direct = rewriteDirectEval(source);
217
+ const reads = rewriteConstructorReads(direct.code, "script", direct.program);
218
+ return rewriteClassicScript(reads.code, reads.program);
219
+ }
220
+
221
+ /** A module's source with direct evals and constructor reads rewritten (see rewriteClassicScriptSource). */
222
+ export function rewriteModuleSource(source) {
223
+ const direct = rewriteDirectEval(source, "module");
224
+ return rewriteConstructorReads(direct.code, "module", direct.program).code;
225
+ }
226
+
227
+ /** Whether a function body or program starts with a "use strict" directive. */
228
+ function hasUseStrict(node) {
229
+ const body = node?.type === "BlockStatement" || node?.type === "Program" ? node.body : [];
230
+ for (const statement of body) {
231
+ if (statement.type !== "ExpressionStatement" || typeof statement.directive !== "string") return false;
232
+ if (statement.directive === "use strict") return true;
233
+ }
234
+ return false;
235
+ }
236
+
237
+ /** The `this` expressions of sloppy-mode functions in a program, in source order (see rewriteClassicScript). */
238
+ function sloppyThisExpressions(program) {
239
+ const found = [];
240
+ const walk = (node, sloppyFunction, strict) => {
241
+ if (!node || typeof node.type !== "string") return;
242
+ switch (node.type) {
243
+ case "ThisExpression":
244
+ if (sloppyFunction) found.push(node);
245
+ return;
246
+ case "FunctionDeclaration":
247
+ case "FunctionExpression": {
248
+ const functionStrict = strict || hasUseStrict(node.body);
249
+ for (const param of node.params) walk(param, !functionStrict, functionStrict);
250
+ walk(node.body, !functionStrict, functionStrict);
251
+ return;
252
+ }
253
+ case "ArrowFunctionExpression": {
254
+ const arrowStrict = strict || hasUseStrict(node.body);
255
+ for (const param of node.params) walk(param, sloppyFunction, arrowStrict);
256
+ walk(node.body, sloppyFunction, arrowStrict);
257
+ return;
258
+ }
259
+ case "ClassDeclaration":
260
+ case "ClassExpression":
261
+ walk(node.superClass, sloppyFunction, strict);
262
+ // Class code is strict.
263
+ walk(node.body, false, true);
264
+ return;
265
+ }
266
+ forEachChild(node, (child) => walk(child, sloppyFunction, strict));
267
+ };
268
+ // A sloppy program's own `this` goes through the helper too: it is the
269
+ // global object SES passes in, unless the evaluator was reached some other way.
270
+ const programStrict = hasUseStrict(program);
271
+ walk(program, !programStrict, programStrict);
272
+ return found.sort((left, right) => left.start - right.start);
133
273
  }
134
274
 
135
275
  /** Names bound by a declaration pattern (identifiers, destructuring, defaults, rest). */
@@ -172,18 +312,20 @@ export const CONSTRUCTOR_HELPER = "$buninu$constructor";
172
312
  *
173
313
  * @param {string} source
174
314
  * @param {"script" | "module"} sourceType
175
- * @returns {{ code: string, rewritten: boolean }}
315
+ * @param {object | null} [parsed] the AST of `source`, when a previous rewrite has it
316
+ * @returns {{ code: string, rewritten: boolean, program: object | null }} `program` is
317
+ * the AST of `code` when the source was left unchanged and parsed
176
318
  */
177
- export function rewriteConstructorReads(source, sourceType = "script") {
319
+ export function rewriteConstructorReads(source, sourceType = "script", parsed = null) {
178
320
  if (!/\)\s*\.\s*constructor\b|\)\s*\[\s*["'`]constructor["'`]\s*\]/.test(source)
179
321
  || !/\basync\b|function\s*\*|\bFunction\b/.test(source)) {
180
- return { code: source, rewritten: false };
322
+ return { code: source, rewritten: false, program: parsed };
181
323
  }
182
324
  let program;
183
325
  try {
184
- program = Parser.parse(source, { ecmaVersion: "latest", sourceType, allowHashBang: true });
326
+ program = parseSource(source, sourceType, parsed);
185
327
  } catch {
186
- return { code: source, rewritten: false };
328
+ return { code: source, rewritten: false, program: null };
187
329
  }
188
330
  const inserts = [];
189
331
  const visit = (node, parent, key) => {
@@ -193,28 +335,82 @@ export function rewriteConstructorReads(source, sourceType = "script") {
193
335
  // $buninu$constructor( <object> ) and drop ".constructor" / ["constructor"],
194
336
  // which may follow the object's closing parenthesis. A call passes true
195
337
  // so the helper keeps the object as the receiver.
338
+ // A `new` callee is parenthesized: `new $buninu$constructor(x)(args)`
339
+ // would construct the helper itself.
196
340
  const call = parent?.type === "CallExpression" && key === "callee";
341
+ const constructed = parent?.type === "NewExpression" && key === "callee";
197
342
  const accessor = source.lastIndexOf(node.computed ? "[" : ".", node.property.start);
198
- inserts.push({ at: node.object.start, text: `${CONSTRUCTOR_HELPER}(`, end: null });
199
- inserts.push({ at: node.object.end, text: call ? ", true)" : ")", end: null });
343
+ // Acorn excludes the receiver's surrounding parentheses from its range.
344
+ // Keep a comma expression inside ONE helper argument, or its second
345
+ // operand would accidentally become the helper's `call` flag.
346
+ const sequence = node.object.type === "SequenceExpression";
347
+ inserts.push({ at: node.object.start, text: `${constructed ? "(" : ""}${CONSTRUCTOR_HELPER}(${sequence ? "(" : ""}`, end: null });
348
+ // The closing comment keeps what was removed, for restoreSourceText.
349
+ const kind = call ? "c" : constructed ? "n" : "p";
350
+ // Text between the object and the removed accessor (a closing parenthesis) stays where it is.
351
+ const gap = accessor - node.object.end;
352
+ const removed = encodeURIComponent(source.slice(accessor, node.end));
353
+ inserts.push({ at: node.object.end, text: `${sequence ? ")" : ""}${call ? ", true" : ""}/*$buninu$ctor:${kind}:${gap}:${removed}${sequence ? ":s" : ""}*/)${constructed ? ")" : ""}`, end: null });
200
354
  inserts.push({ at: accessor, text: "", end: node.end });
201
355
  }
202
356
  if (node.type === "ChainExpression") return;
203
- for (const [name, value] of Object.entries(node)) {
204
- if (name === "type" || name === "start" || name === "end") continue;
205
- for (const child of Array.isArray(value) ? value : [value]) {
206
- if (child && typeof child === "object") visit(child, node, name);
207
- }
208
- }
357
+ forEachChild(node, (child, key) => visit(child, node, key));
209
358
  };
210
359
  visit(program, null, null);
211
- if (!inserts.length) return { code: source, rewritten: false };
360
+ if (!inserts.length) return { code: source, rewritten: false, program };
212
361
  // Apply from the end; at one position the removal goes first so the
213
362
  // insertion lands before what follows it.
214
363
  inserts.sort((left, right) => right.at - left.at || (left.end === null) - (right.end === null));
215
- let code = source;
216
- for (const { at, text, end } of inserts) code = code.slice(0, at) + text + code.slice(end ?? at);
217
- return { code, rewritten: true };
364
+ const code = applyEdits(source, inserts.map(({ at, text, end }) => ({ start: at, end: end ?? at, text })));
365
+ return { code, rewritten: true, program: null };
366
+ }
367
+
368
+ /** Separates the tokens of `-->` and `<!--` in code (see escapeSesHtmlComments). */
369
+ const HTML_COMMENT_MARK = "/*$buninu$h*/";
370
+
371
+ /**
372
+ * SES rejects HTML comment delimiters anywhere in source, including inside
373
+ * strings and regular expressions where browsers treat them as ordinary
374
+ * data. Escape only those lexical values; their evaluated value is unchanged.
375
+ */
376
+ export function escapeSesHtmlComments(source, sourceType = "script") {
377
+ if (!source.includes("<!--") && !source.includes("-->")) return source;
378
+ let program;
379
+ const tokens = [];
380
+ try {
381
+ program = Parser.parse(source, { ecmaVersion: "latest", sourceType, allowHashBang: true, onToken: tokens });
382
+ } catch {
383
+ return source;
384
+ }
385
+ const edits = [];
386
+ // In code, `x-->y` is `x-- > y` and `a<!--b` is `a < !--b`: a comment
387
+ // between the tokens keeps that meaning without the HTML comment text.
388
+ for (let index = 0; index + 1 < tokens.length; index++) {
389
+ const token = tokens[index];
390
+ const next = tokens[index + 1];
391
+ const text = source.slice(token.start, token.end);
392
+ if (text === "--" && next.start === token.end && source[next.start] === ">") {
393
+ edits.push({ start: token.end, end: token.end, text: HTML_COMMENT_MARK });
394
+ } else if (text === "!" && next.start === token.end && source.startsWith("--", next.start)
395
+ && source[token.start - 1] === "<") {
396
+ edits.push({ start: token.end, end: token.end, text: HTML_COMMENT_MARK });
397
+ }
398
+ }
399
+ const visit = (node) => {
400
+ if (!node || typeof node.type !== "string") return;
401
+ if ((node.type === "Literal" && (typeof node.value === "string" || node.regex))
402
+ || node.type === "TemplateElement") {
403
+ const text = source.slice(node.start, node.end);
404
+ const escaped = text.replaceAll("<!--", "<\\x21--").replaceAll("-->", "-\\x2d>");
405
+ if (escaped !== text) edits.push({ start: node.start, end: node.end, text: escaped });
406
+ return;
407
+ }
408
+ forEachChild(node, (child) => visit(child));
409
+ };
410
+ visit(program);
411
+ edits.sort((left, right) => right.start - left.start);
412
+ const code = applyEdits(source, edits);
413
+ return code;
218
414
  }
219
415
 
220
416
  function isConstructorKey(node) {
@@ -230,3 +426,555 @@ function isWrite(parent, key) {
230
426
  if (parent.type === "UnaryExpression" && parent.operator === "delete") return true;
231
427
  return (parent.type === "ForInStatement" || parent.type === "ForOfStatement") && key === "left";
232
428
  }
429
+
430
+ /** The global a direct eval calls instead (see rewriteDirectEval). */
431
+ export const DIRECT_EVAL_HELPER = "$buninu$eval";
432
+ /** The lexical name `this` of the calling code takes inside directly evaluated code. */
433
+ export const DIRECT_EVAL_THIS = "$buninu$this";
434
+ /** Present (true) in a direct eval's scope when the calling code is strict. */
435
+ export const DIRECT_EVAL_STRICT = "$buninu$strict";
436
+ /** Returns the scope object of the direct eval being evaluated (see prepareDirectEvalSource). */
437
+ export const DIRECT_EVAL_SCOPE_HELPER = "$buninu$evalScope";
438
+ /**
439
+ * The direct eval's scope object as a constant of the evaluated code (see
440
+ * prepareDirectEvalSource): `x` from the caller becomes `$buninu$s.x`.
441
+ */
442
+ export const DIRECT_EVAL_SCOPE = "$buninu$s";
443
+ /** Marks a shorthand property `{x}` rewritten to `x: $buninu$s.x`, for restoreSourceText. */
444
+ const SHORTHAND_MARK = "/*$buninu$sh*/";
445
+ /** The page global, bound once per evaluated script (see globalReferenceEdits). */
446
+ export const GLOBAL_BINDING = "$buninu$g";
447
+ /** Throws the ReferenceError an undeclared global read throws. */
448
+ export const REFERENCE_HELPER = "$buninu$ref";
449
+ const GLOBAL_SHORTHAND_MARK = "/*$buninu$gsh*/";
450
+ /**
451
+ * REFERENCE_HELPER's function on `global`: the value of a global whose read
452
+ * gave null or undefined, or the error reading an undeclared variable
453
+ * throws, with Chromium's message.
454
+ */
455
+ export function referenceHelper(global) {
456
+ return function (name) {
457
+ if (name in global) return global[name];
458
+ throw new ReferenceError(`${name} is not defined`);
459
+ };
460
+ }
461
+ // One property read; only a nullish value asks REFERENCE_HELPER whether the name exists.
462
+ const globalRead = (name) => `(${GLOBAL_BINDING}.${name} ?? ${GLOBAL_BINDING}.${REFERENCE_HELPER}(${JSON.stringify(name)}))`;
463
+
464
+ const SCOPE_TYPES = new Set(["BlockStatement", "ForStatement", "ForInStatement", "ForOfStatement", "SwitchStatement",
465
+ "CatchClause", "ClassDeclaration", "ClassExpression", "StaticBlock"]);
466
+
467
+ /**
468
+ * Gives `eval(source)` its direct-eval meaning: the evaluated code sees the
469
+ * caller's local variables.
470
+ *
471
+ * SES evaluates page code through its own evaluator, so a call `eval(x)`
472
+ * made by page code is an indirect eval: `eval("n + 1")` inside a function
473
+ * cannot see the function's `n`. Real code relies on this (template
474
+ * engines, minifier output, integrity checks that evaluate
475
+ * "0,function(){f(t)}" against their own locals). Each direct call site
476
+ * becomes a call of DIRECT_EVAL_HELPER with an object of accessors for every
477
+ * name in scope there; the helper evaluates the source with those accessors
478
+ * as lexical bindings, so reads and assignments reach the caller's variables:
479
+ *
480
+ * function f(n) { return eval("n + 1"); }
481
+ * -> function f(n) { return $buninu$eval({ get n() { return n; }, set n($buninu$v) { n = $buninu$v; },
482
+ * …, $buninu$this: () => this }, "n + 1"); }
483
+ *
484
+ * The real %eval% is never handed to page code. Page code runs in strict
485
+ * mode, where direct eval keeps its own `var`s; that is what this gives.
486
+ * `outerNames` are names the source can already see (when it is itself
487
+ * evaluated by the helper), so nested evals see them too.
488
+ *
489
+ * @param {string} source
490
+ * @param {"script" | "module"} sourceType
491
+ * @param {string[]} [outerNames]
492
+ * @returns {{ code: string, rewritten: boolean }}
493
+ */
494
+ export function rewriteDirectEval(source, sourceType = "script", outerNames = [], outerStrict = false) {
495
+ if (!/\beval\s*\(/.test(source)) return { code: source, rewritten: false, program: null };
496
+ let program;
497
+ // Parsed as the later rewrites parse, so they can reuse the AST; a
498
+ // function body (with a top-level return) is parsed again as such.
499
+ let reusable = true;
500
+ try {
501
+ program = parseSource(source, sourceType);
502
+ } catch {
503
+ try {
504
+ program = Parser.parse(source, { ecmaVersion: "latest", sourceType, allowHashBang: true, allowReturnOutsideFunction: true });
505
+ reusable = false;
506
+ } catch {
507
+ return { code: source, rewritten: false, program: null };
508
+ }
509
+ }
510
+ if (!hasDirectEvalCall(program)) return { code: source, rewritten: false, program: reusable ? program : null };
511
+ const edits = [];
512
+ // Each scope: { names: Set<string>, ordinaryFunction: boolean, strict: boolean }.
513
+ const scopes = [{ names: new Set(outerNames), ordinaryFunction: false, strict: outerStrict || sourceType === "module" }];
514
+
515
+ const visit = (node, parent, key) => {
516
+ if (!node || typeof node.type !== "string") return;
517
+ const isFunction = FUNCTION_TYPES.has(node.type);
518
+ const isScope = isFunction || node.type === "Program" || (SCOPE_TYPES.has(node.type)
519
+ // A function body block shares the function's scope.
520
+ && !(node.type === "BlockStatement" && parent && FUNCTION_TYPES.has(parent.type)));
521
+ if (isScope) {
522
+ const names = new Set();
523
+ if (isFunction) {
524
+ for (const param of node.params) for (const name of boundNames(param)) names.add(name);
525
+ if (node.type === "FunctionExpression" && node.id) names.add(node.id.name);
526
+ if (node.body.type === "BlockStatement") collectDeclarations(node.body.body, names, true);
527
+ } else if (node.type === "Program") {
528
+ collectDeclarations(node.body, names, true);
529
+ } else if (node.type === "CatchClause") {
530
+ for (const name of boundNames(node.param)) names.add(name);
531
+ } else if (node.type === "ClassExpression" && node.id) {
532
+ names.add(node.id.name);
533
+ } else if (node.type === "BlockStatement" || node.type === "StaticBlock") {
534
+ collectDeclarations(node.body, names, false);
535
+ } else if (node.type === "SwitchStatement") {
536
+ collectDeclarations(node.cases.flatMap((switchCase) => switchCase.consequent), names, false);
537
+ } else if (/^For/.test(node.type)) {
538
+ const init = node.type === "ForStatement" ? node.init : node.left;
539
+ if (init?.type === "VariableDeclaration" && init.kind !== "var") {
540
+ for (const declarator of init.declarations) for (const name of boundNames(declarator.id)) names.add(name);
541
+ }
542
+ }
543
+ const enclosing = scopes[scopes.length - 1];
544
+ const strict = enclosing.strict || node.type === "ClassDeclaration" || node.type === "ClassExpression"
545
+ || (node.type === "Program" && hasUseStrict(node)) || (isFunction && hasUseStrict(node.body));
546
+ scopes.push({ names, ordinaryFunction: isFunction ? node.type !== "ArrowFunctionExpression"
547
+ : enclosing.ordinaryFunction, strict });
548
+ }
549
+ if (node.type === "CallExpression" && !node.optional && node.callee.type === "Identifier"
550
+ && node.callee.name === "eval" && !scopes.some((scope) => scope.names.has("eval"))) {
551
+ const names = new Set();
552
+ for (const scope of scopes) for (const name of scope.names) names.add(name);
553
+ names.delete(DIRECT_EVAL_THIS);
554
+ names.delete(DIRECT_EVAL_STRICT);
555
+ const accessors = [...names].map((name) =>
556
+ `get ${name}() { return ${name}; }, set ${name}($buninu$v) { ${name} = $buninu$v; }`);
557
+ if (scopes[scopes.length - 1].ordinaryFunction && !names.has("arguments")) {
558
+ // Inside a getter `arguments` would be the getter's own; read it here.
559
+ accessors.push("arguments: arguments");
560
+ }
561
+ if (scopes[scopes.length - 1].strict) accessors.push(`${DIRECT_EVAL_STRICT}: true`);
562
+ // An arrow function keeps the caller's `this`.
563
+ accessors.push(`${DIRECT_EVAL_THIS}: () => this`);
564
+ const open = source.indexOf("(", node.callee.end);
565
+ edits.push({ start: node.callee.start, end: node.callee.end, text: DIRECT_EVAL_HELPER });
566
+ edits.push({ start: open + 1, end: open + 1, text: `{ ${accessors.join(", ")} }${node.arguments.length ? ", " : ""}` });
567
+ }
568
+ forEachChild(node, (child, key) => visit(child, node, key));
569
+ if (isScope) scopes.pop();
570
+ };
571
+ visit(program, null, null);
572
+ if (!edits.length) return { code: source, rewritten: false, program: reusable ? program : null };
573
+ // From the end; where an insertion and a replacement start at the same
574
+ // place (`eval(eval(x))`), the replacement goes first so the insertion
575
+ // lands before it.
576
+ edits.sort((left, right) => right.start - left.start || right.end - left.end);
577
+ const code = applyEdits(source, edits);
578
+ return { code, rewritten: true, program: null };
579
+ }
580
+
581
+ /** Whether a program calls `eval(...)` directly anywhere (the scope analysis is only needed then). */
582
+ function hasDirectEvalCall(program) {
583
+ let found = false;
584
+ const visit = (node) => {
585
+ if (found) return;
586
+ if (node.type === "CallExpression" && !node.optional && node.callee.type === "Identifier" && node.callee.name === "eval") {
587
+ found = true;
588
+ return;
589
+ }
590
+ forEachChild(node, visit);
591
+ };
592
+ visit(program);
593
+ return found;
594
+ }
595
+
596
+ /**
597
+ * Declarations of a statement list: let/const/class (and function
598
+ * declarations, block-scoped in strict code) always; `var` and nested
599
+ * blocks' function declarations too when the list is a function or program
600
+ * body, where `var` is hoisted to.
601
+ */
602
+ function collectDeclarations(statements, names, functionBody) {
603
+ const hoistVars = (node) => {
604
+ if (!node || typeof node.type !== "string" || FUNCTION_TYPES.has(node.type)
605
+ || node.type === "ClassDeclaration" || node.type === "ClassExpression") return;
606
+ if (node.type === "VariableDeclaration" && node.kind === "var") {
607
+ for (const declarator of node.declarations) for (const name of boundNames(declarator.id)) names.add(name);
608
+ }
609
+ forEachChild(node, (child) => hoistVars(child));
610
+ };
611
+ for (const statement of statements) {
612
+ const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration"
613
+ ? statement.declaration : statement;
614
+ if (!declaration) continue;
615
+ if (declaration.type === "VariableDeclaration" && declaration.kind !== "var") {
616
+ for (const declarator of declaration.declarations) for (const name of boundNames(declarator.id)) names.add(name);
617
+ } else if ((declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") && declaration.id) {
618
+ names.add(declaration.id.name);
619
+ } else if (statement.type === "ImportDeclaration") {
620
+ for (const specifier of statement.specifiers) names.add(specifier.local.name);
621
+ }
622
+ if (functionBody) hoistVars(statement);
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Directly evaluated source: `this` outside its own ordinary functions is
628
+ * the caller's (DIRECT_EVAL_THIS); in sloppy-mode code an ordinary
629
+ * function's `this` gets THIS_REWRITE as in classic scripts; and nested
630
+ * direct evals see `outerNames`.
631
+ *
632
+ * @param {string} source
633
+ * @param {string[]} outerNames
634
+ * @param {boolean} strict whether the calling code is strict
635
+ * @returns {{ code: string, parsed: boolean, strict: boolean }}
636
+ */
637
+ export function prepareDirectEvalSource(source, outerNames, strict = false) {
638
+ const code = rewriteDirectEval(source, "script", outerNames, strict).code;
639
+ let program;
640
+ try {
641
+ program = Parser.parse(code, { ecmaVersion: "latest", sourceType: "script", allowHashBang: true });
642
+ } catch {
643
+ return { code, parsed: false, strict };
644
+ }
645
+ const codeStrict = strict || hasUseStrict(program);
646
+ const edits = [];
647
+ const walk = (node, inFunction, functionStrict) => {
648
+ if (!node || typeof node.type !== "string") return;
649
+ if (node.type === "ThisExpression") {
650
+ if (!inFunction) edits.push({ node, text: `${DIRECT_EVAL_SCOPE}.${DIRECT_EVAL_THIS}()` });
651
+ else if (!functionStrict) edits.push({ node, text: THIS_REWRITE });
652
+ return;
653
+ }
654
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") {
655
+ const own = functionStrict || hasUseStrict(node.body);
656
+ for (const param of node.params) walk(param, true, own);
657
+ walk(node.body, true, own);
658
+ return;
659
+ }
660
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
661
+ walk(node.superClass, inFunction, functionStrict);
662
+ // Class code is strict; its methods have their own `this`.
663
+ walk(node.body, true, true);
664
+ return;
665
+ }
666
+ forEachChild(node, (child) => walk(child, inFunction, functionStrict));
667
+ };
668
+ walk(program, false, codeStrict);
669
+ const outer = new Set(outerNames);
670
+ edits.push(...outerReferenceEdits(program, outer));
671
+ // Globals the evaluated code reads (the handlers a bytecode interpreter
672
+ // evals call its global functions) go through GLOBAL_BINDING, as in classic
673
+ // scripts. Its own top-level declarations stay its variables.
674
+ const globalEdits = !codeStrict && hasBlockFunctionOutsideFunctions(program) ? []
675
+ : globalReferenceEdits(code, program, { exclude: outer, programScope: true });
676
+ edits.push(...globalEdits);
677
+ edits.sort((left, right) => right.node.start - left.node.start);
678
+ const result = applyEdits(code, edits.map(({ node, text }) => ({ start: node.start, end: node.end, text })));
679
+ // The caller's scope object and the global object as constants of the
680
+ // evaluated program, so its functions reach them as closure variables.
681
+ const prologue = `const ${DIRECT_EVAL_SCOPE} = ${DIRECT_EVAL_SCOPE_HELPER}();${globalEdits.length ? ` const ${GLOBAL_BINDING} = globalThis;` : ""} `;
682
+ return { code: prologue + result, parsed: true, strict: codeStrict };
683
+ }
684
+
685
+ /**
686
+ * The caller's variables a directly evaluated program uses, as
687
+ * `$buninu$s.x` (DIRECT_EVAL_SCOPE): the scope object's accessors read and
688
+ * write the caller's variables. Resolved through the evaluator's `with`
689
+ * scopes instead, each use is a dynamic lookup; code that defines functions
690
+ * with eval and runs them in a loop (bytecode interpreters do) would run many
691
+ * times slower. Names the evaluated code declares itself shadow the
692
+ * caller's, as do its own top-level declarations.
693
+ */
694
+ function outerReferenceEdits(program, outerNames) {
695
+ if (!outerNames.size) return [];
696
+ return referenceEdits(program, {
697
+ isOuter: (name) => outerNames.has(name),
698
+ read: (name) => `${DIRECT_EVAL_SCOPE}.${name}`,
699
+ // An identifier call has no receiver. A property call on the scope
700
+ // object would expose that internal object as the callee's `this`.
701
+ callRead: (name) => `(0, ${DIRECT_EVAL_SCOPE}.${name}/*$buninu$call*/)`,
702
+ write: (name) => `${DIRECT_EVAL_SCOPE}.${name}`,
703
+ typeofRead: (name) => `${DIRECT_EVAL_SCOPE}.${name}`,
704
+ shorthand: (name) => `${name}: ${DIRECT_EVAL_SCOPE}.${name}${SHORTHAND_MARK}`,
705
+ withBodies: true,
706
+ });
707
+ }
708
+
709
+ /**
710
+ * A classic script's reads of global variables, as reads of GLOBAL_BINDING's
711
+ * properties. The page realm evaluates scripts inside SES's `with` scopes,
712
+ * where every identifier a function does not declare is a dynamic lookup
713
+ * that the engine cannot cache (JavaScriptCore: about 100 ns each, against
714
+ * about 1 ns for a variable); code that calls global functions in a loop,
715
+ * as bytecode interpreters do, runs many times slower than in a browser.
716
+ * GLOBAL_BINDING is a constant the script declares, so functions reach the
717
+ * global object through an ordinary closure variable.
718
+ *
719
+ * Reads keep their semantics: an undeclared name still throws a
720
+ * ReferenceError (REFERENCE_HELPER), `typeof` of one is "undefined", and a
721
+ * call's `this` is undefined as for a global function in a browser. Writes,
722
+ * `delete` and names inside the page's own `with` statements stay
723
+ * identifiers, as do the engine's `$buninu$` helpers and names written with
724
+ * escapes (restoreSourceText reproduces the page's text from the name).
725
+ */
726
+ function globalReferenceEdits(source, program, { exclude = null, programScope = false } = {}) {
727
+ return referenceEdits(program, {
728
+ isOuter: (name, node) => !name.startsWith("$buninu$") && !exclude?.has(name) && source.slice(node.start, node.end) === name,
729
+ read: globalRead,
730
+ write: null,
731
+ typeofRead: (name) => `${GLOBAL_BINDING}.${name}`,
732
+ shorthand: (name) => `${name}: ${globalRead(name)}${GLOBAL_SHORTHAND_MARK}`,
733
+ withBodies: false,
734
+ programScope,
735
+ });
736
+ }
737
+
738
+ /**
739
+ * Edits for the identifiers of `program` that no scope inside it declares
740
+ * and `isOuter` accepts: reads, `typeof` operands, assignment targets
741
+ * (`write`; null leaves them) and shorthand properties.
742
+ */
743
+ function referenceEdits(program, { isOuter, read, callRead = read, write, typeofRead, shorthand: shorthandText, withBodies, programScope = true }) {
744
+ const edits = [];
745
+ const scopes = [];
746
+ const free = (identifier) => isOuter(identifier.name, identifier) && !scopes.some((names) => names.has(identifier.name));
747
+ const reference = (identifier) => {
748
+ if (!free(identifier)) return false;
749
+ edits.push({ node: identifier, text: read(identifier.name) });
750
+ return true;
751
+ };
752
+ const assigned = (identifier) => {
753
+ if (write && free(identifier)) edits.push({ node: identifier, text: write(identifier.name) });
754
+ };
755
+ const shorthand = (property) => {
756
+ if (!free(property.value)) return;
757
+ edits.push({ node: property, text: shorthandText(property.value.name) });
758
+ };
759
+ const withScope = (names, body) => {
760
+ scopes.push(names);
761
+ body();
762
+ scopes.pop();
763
+ };
764
+ // A binding pattern: only its default values and computed keys are references.
765
+ const binding = (pattern) => {
766
+ if (!pattern) return;
767
+ switch (pattern.type) {
768
+ case "Identifier": return;
769
+ case "AssignmentPattern": binding(pattern.left); expression(pattern.right); return;
770
+ case "ArrayPattern": for (const element of pattern.elements) binding(element); return;
771
+ case "RestElement": binding(pattern.argument); return;
772
+ case "ObjectPattern":
773
+ for (const property of pattern.properties) {
774
+ if (property.type === "RestElement") binding(property.argument);
775
+ else {
776
+ if (property.computed) expression(property.key);
777
+ binding(property.value);
778
+ }
779
+ }
780
+ return;
781
+ default: expression(pattern);
782
+ }
783
+ };
784
+ // An assignment target: its identifiers are references.
785
+ const target = (pattern) => {
786
+ if (!pattern) return;
787
+ switch (pattern.type) {
788
+ case "Identifier": assigned(pattern); return;
789
+ case "AssignmentPattern": target(pattern.left); expression(pattern.right); return;
790
+ case "ArrayPattern": for (const element of pattern.elements) target(element); return;
791
+ case "RestElement": target(pattern.argument); return;
792
+ case "ObjectPattern":
793
+ for (const property of pattern.properties) {
794
+ if (property.type === "RestElement") target(property.argument);
795
+ else if (property.shorthand && property.value.type === "Identifier") { if (write) shorthand(property); }
796
+ else {
797
+ if (property.computed) expression(property.key);
798
+ target(property.value);
799
+ }
800
+ }
801
+ return;
802
+ default: expression(pattern);
803
+ }
804
+ };
805
+ const functionNode = (node) => {
806
+ const names = new Set();
807
+ for (const param of node.params) for (const name of boundNames(param)) names.add(name);
808
+ if (node.type === "FunctionExpression" && node.id) names.add(node.id.name);
809
+ if (node.type !== "ArrowFunctionExpression") names.add("arguments");
810
+ if (node.body.type === "BlockStatement") collectDeclarations(node.body.body, names, true);
811
+ withScope(names, () => {
812
+ for (const param of node.params) binding(param);
813
+ if (node.body.type === "BlockStatement") for (const statement of node.body.body) expression(statement);
814
+ else expression(node.body);
815
+ });
816
+ };
817
+ const lexicalNames = (declaration) => {
818
+ const names = new Set();
819
+ if (declaration?.type === "VariableDeclaration" && declaration.kind !== "var") {
820
+ for (const declarator of declaration.declarations) for (const name of boundNames(declarator.id)) names.add(name);
821
+ }
822
+ return names;
823
+ };
824
+ const expression = (node) => {
825
+ if (!node || typeof node.type !== "string") return;
826
+ switch (node.type) {
827
+ case "Identifier": reference(node); return;
828
+ case "CallExpression": case "TaggedTemplateExpression": {
829
+ const callee = node.type === "CallExpression" ? node.callee : node.tag;
830
+ if (callee.type === "Identifier" && free(callee)) {
831
+ edits.push({ node: callee, text: callRead(callee.name) });
832
+ } else expression(callee);
833
+ if (node.type === "CallExpression") for (const argument of node.arguments) expression(argument);
834
+ else expression(node.quasi);
835
+ return;
836
+ }
837
+ case "FunctionDeclaration": case "FunctionExpression": case "ArrowFunctionExpression": functionNode(node); return;
838
+ case "MemberExpression": expression(node.object); if (node.computed) expression(node.property); return;
839
+ case "Property":
840
+ if (node.computed) expression(node.key);
841
+ if (node.shorthand && node.value.type === "Identifier") shorthand(node);
842
+ else expression(node.value);
843
+ return;
844
+ case "MethodDefinition": case "PropertyDefinition":
845
+ if (node.computed) expression(node.key);
846
+ expression(node.value);
847
+ return;
848
+ case "LabeledStatement": expression(node.body); return;
849
+ case "BreakStatement": case "ContinueStatement": case "MetaProperty": return;
850
+ case "VariableDeclaration":
851
+ for (const declarator of node.declarations) { binding(declarator.id); expression(declarator.init); }
852
+ return;
853
+ case "AssignmentExpression":
854
+ // A compound assignment reads its target too; without `write` it stays as written.
855
+ if (node.left.type === "Identifier" && node.operator !== "=" && write) reference(node.left);
856
+ else target(node.left);
857
+ expression(node.right);
858
+ return;
859
+ case "UpdateExpression":
860
+ if (node.argument.type === "Identifier") { if (write) reference(node.argument); }
861
+ else expression(node.argument);
862
+ return;
863
+ case "UnaryExpression":
864
+ if (node.argument.type === "Identifier" && node.operator === "typeof") {
865
+ if (free(node.argument)) edits.push({ node: node.argument, text: typeofRead(node.argument.name) });
866
+ return;
867
+ }
868
+ if (node.argument.type === "Identifier" && node.operator === "delete") {
869
+ if (write) reference(node.argument);
870
+ return;
871
+ }
872
+ expression(node.argument);
873
+ return;
874
+ case "WithStatement":
875
+ expression(node.object);
876
+ if (withBodies) expression(node.body);
877
+ return;
878
+ case "ClassDeclaration": case "ClassExpression":
879
+ withScope(new Set(node.id ? [node.id.name] : []), () => { expression(node.superClass); expression(node.body); });
880
+ return;
881
+ case "BlockStatement": case "StaticBlock": {
882
+ const names = new Set();
883
+ collectDeclarations(node.body, names, false);
884
+ withScope(names, () => { for (const statement of node.body) expression(statement); });
885
+ return;
886
+ }
887
+ case "ForStatement":
888
+ withScope(lexicalNames(node.init), () => {
889
+ expression(node.init); expression(node.test); expression(node.update); expression(node.body);
890
+ });
891
+ return;
892
+ case "ForInStatement": case "ForOfStatement":
893
+ withScope(lexicalNames(node.left), () => {
894
+ if (node.left.type === "VariableDeclaration") expression(node.left);
895
+ else target(node.left);
896
+ expression(node.right);
897
+ expression(node.body);
898
+ });
899
+ return;
900
+ case "CatchClause":
901
+ withScope(new Set(boundNames(node.param)), () => { binding(node.param); expression(node.body); });
902
+ return;
903
+ case "SwitchStatement": {
904
+ expression(node.discriminant);
905
+ const names = new Set();
906
+ collectDeclarations(node.cases.flatMap((switchCase) => switchCase.consequent), names, false);
907
+ withScope(names, () => { for (const switchCase of node.cases) forEachChild(switchCase, expression); });
908
+ return;
909
+ }
910
+ default: forEachChild(node, expression);
911
+ }
912
+ };
913
+ // A classic script's top-level declarations are globals (rewriteClassicScript lifts them).
914
+ const programNames = new Set();
915
+ if (programScope) collectDeclarations(program.body, programNames, true);
916
+ withScope(programNames, () => { for (const statement of program.body) expression(statement); });
917
+ return edits;
918
+ }
919
+
920
+ /**
921
+ * Whether sloppy code declares a function in a block outside any function:
922
+ * Annex B gives it a var binding as well, which the global rewrite would
923
+ * not see, so such scripts keep their identifiers.
924
+ */
925
+ function hasBlockFunctionOutsideFunctions(program) {
926
+ let found = false;
927
+ const visit = (node, inBlock) => {
928
+ if (found || !node || typeof node.type !== "string") return;
929
+ if (node.type === "FunctionDeclaration" && inBlock) { found = true; return; }
930
+ if (FUNCTION_TYPES.has(node.type) || node.type === "ClassBody") return;
931
+ const block = node.type === "BlockStatement" || node.type === "SwitchCase" || node.type === "StaticBlock";
932
+ forEachChild(node, (child) => visit(child, inBlock || block || (node.type !== "Program" && child?.type === "FunctionDeclaration")));
933
+ };
934
+ visit(program, false);
935
+ return found;
936
+ }
937
+
938
+ const GLOBAL_READ = /\(\$buninu\$g\.([^\s?]+) \?\? \$buninu\$g\.\$buninu\$ref\("\1"\)\)/g;
939
+ const GLOBAL_SHORTHAND = /([^\s{,:]+): \(\$buninu\$g\.\1 \?\? \$buninu\$g\.\$buninu\$ref\("\1"\)\)\/\*\$buninu\$gsh\*\//g;
940
+ const SHORTHAND_PROPERTY = /([^\s{,:]+): \$buninu\$s\.\1\/\*\$buninu\$sh\*\//g;
941
+ const DIRECT_EVAL_CALL = /\$buninu\$eval\(\{ [^]*?\$buninu\$this: \(\) => this \}(?:, )?/g;
942
+ const CONSTRUCTOR_CLOSE = /\/\*\$buninu\$ctor:([pcn]):(\d+):([^*]*?)(:s)?\*\/\)/;
943
+
944
+ /**
945
+ * The text a page wrote, from the text of code this module rewrote: what
946
+ * Function.prototype.toString shows for page functions. Each rewrite marks
947
+ * what it inserted, so this undoes exactly those edits (and SES's own
948
+ * `import(` evasion).
949
+ *
950
+ * @param {string} text
951
+ */
952
+ export function restoreSourceText(text) {
953
+ if (!text.includes("$buninu$") && !text.includes("__import__") && !text.includes("\\x21--")
954
+ && !text.includes("\\x2d>")) return text;
955
+ let code = text;
956
+ if (code.includes(GLOBAL_BINDING)) {
957
+ code = code.replace(GLOBAL_SHORTHAND, "$1").replace(GLOBAL_READ, "$1").replaceAll(`${GLOBAL_BINDING}.`, "");
958
+ }
959
+ code = code.replaceAll(THIS_REWRITE, "this").replaceAll(`${DIRECT_EVAL_THIS}()`, "this");
960
+ if (code.includes(`${DIRECT_EVAL_SCOPE}.`)) {
961
+ code = code.replace(/\(0, \$buninu\$s\.([^\s()]+)\/\*\$buninu\$call\*\/\)/g, "$1")
962
+ .replace(SHORTHAND_PROPERTY, "$1").replaceAll(`${DIRECT_EVAL_SCOPE}.`, "");
963
+ }
964
+ for (let match; (match = CONSTRUCTOR_CLOSE.exec(code));) {
965
+ const open = code.lastIndexOf(`${CONSTRUCTOR_HELPER}(`, match.index);
966
+ if (open < 0) break;
967
+ const [, kind, gapLength, removed, sequence] = match;
968
+ let object = code.slice(open + CONSTRUCTOR_HELPER.length + 1, match.index);
969
+ if (kind === "c" && object.endsWith(", true")) object = object.slice(0, -", true".length);
970
+ if (sequence) object = object.slice(1, -1);
971
+ const start = kind === "n" ? open - 1 : open;
972
+ const end = match.index + match[0].length + (kind === "n" ? 1 : 0);
973
+ const gap = code.slice(end, end + Number(gapLength));
974
+ code = code.slice(0, start) + object + gap + decodeURIComponent(removed) + code.slice(end + Number(gapLength));
975
+ }
976
+ return code.replace(DIRECT_EVAL_CALL, "eval(")
977
+ .replaceAll(HTML_COMMENT_MARK, "")
978
+ .replaceAll("<\\x21--", "<!--").replaceAll("-\\x2d>", "-->")
979
+ .replace(/\b__import__(?=\s*(?:\(|\/[/*]))/g, "import");
980
+ }