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.
- package/AGENTS.md +4 -0
- package/README.md +74 -9
- package/ai-debug.log +5537 -0
- package/bunfig.toml +1 -0
- package/buninu-browser.js +4 -0
- package/index.js +5 -0
- package/lib/audio/piano.js +24 -1
- package/lib/audio/playback-buffer.js +27 -0
- package/lib/audio/pulse-client.js +137 -19
- package/lib/cdp-server.js +22 -11
- package/lib/happy-dom/parser.js +29 -4
- package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
- package/lib/headless-shell.js +69 -19
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +268 -85
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/media/video-keyframe.js +29 -0
- package/lib/network/network-service.js +161 -14
- package/lib/paint/display-list.js +316 -15
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +19 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +995 -69
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/compositor.js +16 -1
- package/lib/renderer/custom-element-reactions.js +18 -0
- package/lib/renderer/history.js +42 -0
- package/lib/renderer/host.js +166 -9
- package/lib/renderer/image-loading.js +27 -0
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/keyboard-input.js +12 -0
- package/lib/renderer/main.js +162 -12
- package/lib/renderer/media-playback-events.js +13 -0
- package/lib/renderer/media-source.js +819 -0
- package/lib/renderer/messaging.js +411 -0
- package/lib/renderer/native-functions.js +113 -0
- package/lib/renderer/page-realm.js +1408 -242
- package/lib/renderer/page-renderer.js +1893 -223
- package/lib/renderer/piano-input.js +22 -0
- package/lib/renderer/pointer-move.js +29 -0
- package/lib/renderer/rejections.js +31 -0
- package/lib/renderer/rewrite-worker.js +22 -0
- package/lib/renderer/script-rewrite.js +805 -57
- package/lib/renderer/trusted-types.js +129 -0
- package/lib/renderer/video-presentation.js +126 -0
- package/lib/renderer/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +186 -29
- package/lib/user-agent.js +14 -2
- package/package.json +4 -1
- package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
- package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
- package/patches/happy-dom/README.md +26 -0
- package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
- package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
- package/patches/ses/README.md +4 -0
- package/test/canvas-2d.test.js +55 -0
- package/test/compositor.test.js +41 -1
- package/test/computed-style.test.js +59 -1
- package/test/display-list.test.js +88 -1
- package/test/download.test.js +43 -0
- package/test/fixtures/long-video.mp4 +0 -0
- package/test/fixtures/media-sync.js +57 -0
- package/test/happy-dom-platform-adapters.test.js +25 -0
- package/test/headless-shell.test.js +23 -1
- package/test/hit-test.test.js +25 -1
- package/test/media-playback-events.test.js +22 -0
- package/test/media-source.test.js +156 -0
- package/test/media-sync.test.js +12 -0
- package/test/mobile.test.js +2 -2
- package/test/navigation.test.js +61 -0
- package/test/network-service.test.js +14 -0
- package/test/page-scripts.test.js +1203 -7
- package/test/piano-input.test.js +44 -0
- package/test/playback-buffer.test.js +31 -0
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +84 -0
- package/test/renderer-host.test.js +5 -2
- package/test/screenshot-cli.test.js +25 -1
- package/test/script-rewrite.test.js +111 -6
- package/test/setup.js +5 -0
- package/test/text-layout.test.js +62 -0
- package/test/user-agent.test.js +60 -4
- package/test/video-presentation.test.js +67 -0
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
- 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
|
-
* @
|
|
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 =
|
|
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 =
|
|
54
|
-
const value =
|
|
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} = ${
|
|
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} = ${
|
|
166
|
+
edits.push({ start: node.start, end: node.end, text: `${node.id.name} = ${text(node.start, node.end)};` });
|
|
83
167
|
return;
|
|
84
168
|
}
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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 =
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|