mirror of
https://github.com/HeyPuter/browser.js.git
synced 2026-11-13 18:33:58 -10:00
[core] update all of core to use safe iteration
Brings the safe-iteration work over from the now-outdated feat/safe-iteration (9d627e36, c0332df0) and applies it to everything that has landed since. That branch's other commit, 91264d0a, is already on develop through the stack. `for...of`, spread and array destructuring all run the iteration protocol, whose lookups land on `Array.prototype[@@iterator]` and `%IteratorPrototype%` - both writable by the page. Any of them in the page realm lets a page change what scramjet believes it read, or what it passes on after checking: `el.prepend(...args)` spread `args` into the native after `text.around` had inspected them, so a replaced array iterator could hand the native a different node. - `drain(xs)` in the snapshot: a for...of-able view of an array built from own properties, so none of the protocol's lookups reach the page. - `no-unsafe-iteration` lint rule, at "error" for src/client and src/Tap.ts, with tests. Its messages now point at `drain` and `Reflect_apply`. - All 45 sites it found: spreads into natives become `Reflect_apply(super.m, this, args)` (node.ts, reflect.ts, css.ts, settimeout, importScripts); loops become `drain`; the NamedNodeMap fallback indexes the native rather than running its iterator; Headers are copied with the native's own `forEach`. The brand-check plugin learns that `Reflect_apply(super.m, this, ...)` calls the native. `observedAttributes` keeps `for...of`: it is the page's own iterable, and the browser runs its protocol too. - Things the rule cannot see: `Promise_all` still walked its argument with the protocol and called each element's `then`; it is now a snapshot helper over `Promise_then`, and the unused `Promise_race`, `Promise_allSettled` and `Promise_any` are gone. Tap.dispatch, which runs in the page on every navigation, drops spread, `.map`, `.find`, `.includes` and `.push`. `Array.prototype` method calls in client.ts, chrome.ts, event.ts, err.ts and webidl.ts go through the snapshot, and `client.native`'s getters and setters, and sourcemaps' toString, stop going through `Function.prototype.call`. - Cold indexed loops that only read `xs[i]` read as `for (const x of drain(xs))` now. Hot paths stay indexed: the text layer, per-write attribute rules, instanceof, per-call IDL conversion, stack formatting and performance entry lists. So do live platform collections, which `drain` cannot be handed. - The commented-out `window.postMessage` proxy from the wrapPostMessage era is deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01AWUAE5wuAo6WKM7y5TncPC
This commit is contained in:
1 parent
f7268b8c99
commit
c5d59ce573
31 files changed
+631
-257
No files matched your search
@@ -8,6 +8,7 @@ import noGlobalsPlugin from "./tools/eslint/no-globals-plugin.mjs";
|
||||
import noInstanceofPlugin from "./tools/eslint/no-instanceof-plugin.mjs";
|
||||
import poisonedCtxPlugin from "./tools/eslint/poisoned-ctx-plugin.mjs";
|
||||
import brandCheckPlugin from "./tools/eslint/brand-check-plugin.mjs";
|
||||
import noUnsafeIterationPlugin from "./tools/eslint/no-unsafe-iteration-plugin.mjs";
|
||||
|
||||
const scramjetCorePlugin = {
|
||||
rules: {
|
||||
@@ -15,6 +16,7 @@ const scramjetCorePlugin = {
|
||||
...noInstanceofPlugin.rules,
|
||||
...poisonedCtxPlugin.rules,
|
||||
...brandCheckPlugin.rules,
|
||||
...noUnsafeIterationPlugin.rules,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -99,9 +101,20 @@ export default [
|
||||
},
|
||||
},
|
||||
{
|
||||
// the page realm, where the iteration intrinsics are page-replaceable.
|
||||
// `src/shared/` is deliberately not covered: most of it runs in the
|
||||
// service worker, where there is no page script to replace anything
|
||||
files: ["src/client/**/*.ts"],
|
||||
rules: {
|
||||
"scramjet-core/no-poisoned-ctx-value": "warn",
|
||||
"scramjet-core/no-unsafe-iteration": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
// shared, but dispatched in the page on every navigation and pushState
|
||||
files: ["src/Tap.ts"],
|
||||
rules: {
|
||||
"scramjet-core/no-unsafe-iteration": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Promise_all } from "@/shared/snapshot";
|
||||
import {
|
||||
Array_includes,
|
||||
Array_map,
|
||||
Object_create,
|
||||
Promise_all,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
type Description = {
|
||||
context?: object;
|
||||
@@ -43,50 +49,66 @@ function mergeTapOrder(plugin: Plugin, order?: TapOrder): TapOrder {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Order `callbacks` so each runs after the plugins it names in `after`, and
|
||||
* before the ones it names in `before`.
|
||||
*
|
||||
* Dispatch happens in the page realm - every navigation and `pushState` goes
|
||||
* through here - so this sticks to `drain` and indexed writes: `for...of`,
|
||||
* spread and the `Array.prototype` methods all reach intrinsics the page can
|
||||
* replace, and with them which hooks run, in what order.
|
||||
*/
|
||||
function sortCallbacks<T extends Description>(
|
||||
callbacks: CallbackInfo<T>[]
|
||||
): CallbackInfo<T>[] {
|
||||
const afters: Record<string, string[]> = {};
|
||||
for (const callback of callbacks) {
|
||||
const afters: Record<string, string[]> = Object_create(null);
|
||||
const addAfter = (plugin: string, after: string) => {
|
||||
const list = (afters[plugin] ??= []);
|
||||
if (!Array_includes(list, after)) list[list.length] = after;
|
||||
};
|
||||
for (const callback of drain(callbacks)) {
|
||||
if (callback.order.before) {
|
||||
for (const before of callback.order.before) {
|
||||
afters[before] ??= [];
|
||||
if (!afters[before].includes(callback.plugin.name)) {
|
||||
afters[before].push(callback.plugin.name);
|
||||
}
|
||||
for (const before of drain(callback.order.before)) {
|
||||
addAfter(before, callback.plugin.name);
|
||||
}
|
||||
}
|
||||
if (callback.order.after) {
|
||||
for (const after of callback.order.after) {
|
||||
afters[callback.plugin.name] ??= [];
|
||||
if (!afters[callback.plugin.name].includes(after)) {
|
||||
afters[callback.plugin.name].push(after);
|
||||
}
|
||||
for (const after of drain(callback.order.after)) {
|
||||
addAfter(callback.plugin.name, after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const byName = (name: string) => {
|
||||
for (const callback of drain(callbacks)) {
|
||||
if (callback.plugin.name === name) return callback;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const sorted: CallbackInfo<T>[] = [];
|
||||
function recurse(callback: CallbackInfo<T>, visited: string[]) {
|
||||
if (afters[callback.plugin.name]) {
|
||||
for (const after of afters[callback.plugin.name]) {
|
||||
if (visited.includes(after)) {
|
||||
throw `Circular dependency detected: ${callback.plugin.name} -> ${after}. Using append order.`;
|
||||
}
|
||||
const afterCallback = callbacks.find((c) => c.plugin.name === after);
|
||||
if (afterCallback) {
|
||||
recurse(afterCallback, [...visited, callback.plugin.name]);
|
||||
// the chain of plugins being placed, innermost last
|
||||
const visiting: string[] = [];
|
||||
function recurse(callback: CallbackInfo<T>) {
|
||||
const name = callback.plugin.name;
|
||||
if (afters[name]) {
|
||||
visiting[visiting.length] = name;
|
||||
for (const after of drain(afters[name])) {
|
||||
if (Array_includes(visiting, after)) {
|
||||
throw `Circular dependency detected: ${name} -> ${after}. Using append order.`;
|
||||
}
|
||||
const afterCallback = byName(after);
|
||||
if (afterCallback) recurse(afterCallback);
|
||||
}
|
||||
visiting.length--;
|
||||
}
|
||||
if (!sorted.includes(callback)) {
|
||||
sorted.push(callback);
|
||||
}
|
||||
if (!Array_includes(sorted, callback)) sorted[sorted.length] = callback;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const callback of callbacks) {
|
||||
recurse(callback, []);
|
||||
for (const callback of drain(callbacks)) {
|
||||
recurse(callback);
|
||||
}
|
||||
return sorted;
|
||||
} catch (err) {
|
||||
@@ -117,13 +139,12 @@ export class Tap {
|
||||
props: T["props"]
|
||||
): Promise<void[]> | null {
|
||||
const internal = hook as unknown as InternalHookDescription;
|
||||
let callbacks = internal.tap.callbacks[internal.key];
|
||||
const callbacks = internal.tap.callbacks[internal.key];
|
||||
if (!callbacks || callbacks.length === 0) return null;
|
||||
|
||||
callbacks = sortCallbacks([...callbacks] as CallbackInfo<T>[]);
|
||||
const sorted = sortCallbacks(callbacks as CallbackInfo<T>[]);
|
||||
|
||||
const results = callbacks.map((cb) => cb.callback(context, props));
|
||||
return Promise_all(results);
|
||||
return Promise_all(Array_map(sorted, (cb) => cb.callback(context, props)));
|
||||
}
|
||||
|
||||
static tap<T extends Description>(
|
||||
@@ -135,11 +156,8 @@ export class Tap {
|
||||
const internal = hook as unknown as InternalHookDescription;
|
||||
const callbacks = internal.tap.callbacks;
|
||||
if (!callbacks[internal.key]) callbacks[internal.key] = [];
|
||||
callbacks[internal.key]!.push({
|
||||
callback,
|
||||
plugin,
|
||||
order,
|
||||
});
|
||||
const list = callbacks[internal.key]!;
|
||||
list[list.length] = { callback, plugin, order };
|
||||
}
|
||||
|
||||
static create<T extends Record<string, Description>>(): TapInstance<T> {
|
||||
@@ -161,6 +179,6 @@ export class Tap {
|
||||
|
||||
static getTappers<T extends Description>(hook: T): Plugin[] {
|
||||
const internal = hook as unknown as InternalHookDescription;
|
||||
return internal.tap.callbacks[internal.key].map((c) => c.plugin);
|
||||
return Array_map(internal.tap.callbacks[internal.key], (c) => c.plugin);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
String_startsWith,
|
||||
String_substring,
|
||||
String_toLowerCase,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
/**
|
||||
@@ -537,6 +538,9 @@ export class AttributeLayer {
|
||||
callback = definition.prototype.attributeChangedCallback;
|
||||
const observed = (definition as any).observedAttributes;
|
||||
if (observed === undefined || observed === null) return;
|
||||
// the page's own iterable, so its own iteration protocol is the one
|
||||
// the browser would run too, when `define()` converts it
|
||||
// eslint-disable-next-line scramjet-core/no-unsafe-iteration
|
||||
for (const entry of observed as Iterable<unknown>) {
|
||||
if (String(entry) === name) listed = true;
|
||||
}
|
||||
@@ -560,8 +564,8 @@ export class AttributeLayer {
|
||||
).getAttributeNames();
|
||||
|
||||
let internal = false;
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
if (isInternalAttribute(all[i])) {
|
||||
for (const name of drain(all)) {
|
||||
if (isInternalAttribute(name)) {
|
||||
internal = true;
|
||||
break;
|
||||
}
|
||||
@@ -569,8 +573,7 @@ export class AttributeLayer {
|
||||
if (!internal) return all;
|
||||
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
const name = all[i];
|
||||
for (const name of drain(all)) {
|
||||
const mirrored = mirroredAttributeName(name);
|
||||
|
||||
// an ordinary attribute, in its own position
|
||||
|
||||
@@ -60,11 +60,13 @@ import {
|
||||
Object_setPrototypeOf,
|
||||
Object_assign,
|
||||
Promise_then,
|
||||
drain,
|
||||
String_startsWith,
|
||||
String_trim,
|
||||
String_split,
|
||||
String_toLowerCase,
|
||||
_RegExp,
|
||||
Array_includes,
|
||||
} from "@/shared/snapshot";
|
||||
import {
|
||||
isConstructorMember,
|
||||
@@ -182,8 +184,8 @@ export type ScramjetModule = {
|
||||
};
|
||||
|
||||
function findBox(global: Window, seen: Window[]): SingletonBox | null {
|
||||
if (seen.includes(global)) return null;
|
||||
seen.push(global);
|
||||
if (Array_includes(seen, global)) return null;
|
||||
seen[seen.length] = global;
|
||||
|
||||
try {
|
||||
if ((SCRAMJETCLIENT in global) as any) {
|
||||
@@ -339,7 +341,9 @@ export class ScramjetClient {
|
||||
Reflect_apply(fn, object, args),
|
||||
});
|
||||
} else if (desc.get) {
|
||||
return desc.get.call(object);
|
||||
// not `desc.get.call`: that looks `call` up on
|
||||
// Function.prototype, which the page can replace
|
||||
return Reflect_apply(desc.get, object, []);
|
||||
}
|
||||
},
|
||||
set(_target, method: string, value: any) {
|
||||
@@ -349,7 +353,7 @@ export class ScramjetClient {
|
||||
`No native setter ${method.toString()} found for ${prototype}`
|
||||
);
|
||||
}
|
||||
desc.set.call(object, value);
|
||||
Reflect_apply(desc.set, object, [value]);
|
||||
return true;
|
||||
},
|
||||
}
|
||||
@@ -378,7 +382,7 @@ export class ScramjetClient {
|
||||
* there is no setter to find and the key is defined outright.
|
||||
*/
|
||||
saveNatives() {
|
||||
for (const key of Object_getOwnPropertyNames(this.global)) {
|
||||
for (const key of drain(Object_getOwnPropertyNames(this.global))) {
|
||||
const value = this.global[key];
|
||||
if (typeof value === "function" && "prototype" in value) {
|
||||
const natives = Object_create(null);
|
||||
@@ -510,12 +514,20 @@ export class ScramjetClient {
|
||||
if (!iswindow) return "";
|
||||
// TODO: need to nullify the actual meta tag so it still sends unsafe-url
|
||||
const nDoc = new client.native.Document(client.global.document);
|
||||
const meta = [
|
||||
...nDoc.querySelectorAll("meta[name='referrer']"),
|
||||
...nDoc.querySelectorAll("meta[name='referrer-policy']"),
|
||||
...nDoc.querySelectorAll("meta[http-equiv='referrer-policy']"),
|
||||
];
|
||||
const last = meta[meta.length - 1];
|
||||
// only the last match counts, so look for it list by list from the
|
||||
// back. Indexed, and not `drain`: a NodeList is a live platform
|
||||
// collection, and spreading it ran the page-replaceable
|
||||
// iteration protocol over what decides the referrer policy
|
||||
let last: Element | undefined;
|
||||
for (const selector of drain([
|
||||
"meta[http-equiv='referrer-policy']",
|
||||
"meta[name='referrer-policy']",
|
||||
"meta[name='referrer']",
|
||||
])) {
|
||||
const list = nDoc.querySelectorAll(selector);
|
||||
last = list[list.length - 1];
|
||||
if (last) break;
|
||||
}
|
||||
if (last) {
|
||||
const nLast = new client.native.HTMLMetaElement(last);
|
||||
return nLast.getAttribute("content");
|
||||
@@ -549,7 +561,7 @@ export class ScramjetClient {
|
||||
|
||||
const modules: ScramjetModule[] = [];
|
||||
|
||||
for (const key of context.keys()) {
|
||||
for (const key of drain(context.keys())) {
|
||||
if (!key.endsWith(".ts")) continue;
|
||||
if (
|
||||
(key.startsWith("./dom/") && "window" in this.global) ||
|
||||
@@ -567,7 +579,7 @@ export class ScramjetClient {
|
||||
return aorder - border;
|
||||
});
|
||||
|
||||
for (const module of modules) {
|
||||
for (const module of drain(modules)) {
|
||||
// one module throwing used to abort the loop, so a single interface
|
||||
// missing from this realm silently left every module after it
|
||||
// uninstalled. a hooked-but-incomplete realm is bad; an unhooked one
|
||||
@@ -804,8 +816,8 @@ export class ScramjetClient {
|
||||
String_toLowerCase(sandbox),
|
||||
new _RegExp("[\\t\\n\\f\\r ]+")
|
||||
);
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i] === "allow-same-origin") return false;
|
||||
for (const token of drain(tokens)) {
|
||||
if (token === "allow-same-origin") return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -814,6 +826,22 @@ export class ScramjetClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `"Element.prototype.innerHTML"` as the object that owns the last name
|
||||
* and that name, walked from this client's global. Null when a step along
|
||||
* the way is missing from this realm.
|
||||
*/
|
||||
private resolvePath(name: string): { owner: any; prop: string } | null {
|
||||
const path = String_split(name, ".");
|
||||
const prop = path[path.length - 1];
|
||||
let owner: any = this.global;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
owner = owner?.[path[i]];
|
||||
}
|
||||
|
||||
return owner && prop ? { owner, prop } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This document's origin as `postMessage` compares it: the serialized
|
||||
* origin for a tuple origin, or for an opaque one a key unique to it -
|
||||
@@ -856,20 +884,17 @@ export class ScramjetClient {
|
||||
): void;
|
||||
Proxy(name: string | string[], handler: Proxy<any>): void {
|
||||
if (Array_isArray(name)) {
|
||||
for (const n of name) {
|
||||
for (const n of drain(name)) {
|
||||
this.Proxy(n, handler);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const split = name.split(".");
|
||||
const prop = split.pop();
|
||||
const target = split.reduce((a, b) => a?.[b], this.global);
|
||||
const target = this.resolvePath(name);
|
||||
if (!target) return;
|
||||
if (!prop) return;
|
||||
|
||||
this.RawProxy(target, prop, handler, name);
|
||||
this.RawProxy(target.owner, target.prop, handler, name);
|
||||
}
|
||||
/**
|
||||
* A named `apply`/`construct` pair for one intercepted member.
|
||||
@@ -1150,20 +1175,17 @@ return { apply, construct };
|
||||
): void;
|
||||
Trap(name: string | string[], descriptor: Trap<any>): void {
|
||||
if (Array_isArray(name)) {
|
||||
for (const n of name) {
|
||||
for (const n of drain(name)) {
|
||||
this.Trap(n, descriptor);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const split = name.split(".");
|
||||
const prop = split.pop();
|
||||
const target = split.reduce((a, b) => a?.[b], this.global);
|
||||
const target = this.resolvePath(name);
|
||||
if (!target) return;
|
||||
if (!prop) return;
|
||||
|
||||
this.RawTrap(target, prop, descriptor, name);
|
||||
this.RawTrap(target.owner, target.prop, descriptor, name);
|
||||
}
|
||||
RawTrap(
|
||||
target: any,
|
||||
@@ -1460,13 +1482,13 @@ return { apply, construct };
|
||||
return "value" in desc && desc.writable === false;
|
||||
};
|
||||
|
||||
for (const prop of Reflect_ownKeys(prototypeDescs)) {
|
||||
for (const prop of drain(Reflect_ownKeys(prototypeDescs))) {
|
||||
const classDesc = prototypeDescs[prop];
|
||||
if (isClassMetadata(prop, classDesc, false)) continue;
|
||||
if (isConstructorMember(classDesc.value)) continue;
|
||||
writePrototypeField(prop, baseclass.prototype, classDesc, true);
|
||||
}
|
||||
for (const prop of Reflect_ownKeys(staticDescs)) {
|
||||
for (const prop of drain(Reflect_ownKeys(staticDescs))) {
|
||||
const handlerDesc = staticDescs[prop];
|
||||
if (isClassMetadata(prop, handlerDesc, true)) continue;
|
||||
const value = handlerDesc.value;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
String_substring,
|
||||
String_toLowerCase,
|
||||
TypeError,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
import { ScramjetClient } from "@client/client";
|
||||
import {
|
||||
@@ -77,9 +78,9 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
const cookies = client.context.cookieJar.getCookieList(client.url, true);
|
||||
const items: CookieListItem[] = [];
|
||||
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
if (name !== undefined && cookies[i].name !== name) continue;
|
||||
items[items.length] = toCookieListItem(cookies[i]);
|
||||
for (const cookie of drain(cookies)) {
|
||||
if (name !== undefined && cookie.name !== name) continue;
|
||||
items[items.length] = toCookieListItem(cookie);
|
||||
}
|
||||
|
||||
return items;
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
String,
|
||||
Symbol_iterator,
|
||||
_Map,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
/**
|
||||
@@ -127,14 +128,16 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
return function* (this: NamedNodeMap) {
|
||||
const owner = ownerOf(brand(this));
|
||||
if (!owner) {
|
||||
const iterator = new client.native.NamedNodeMap(real(this))[
|
||||
Symbol_iterator
|
||||
]();
|
||||
for (const attr of iterator) yield attr;
|
||||
// indexed through the native, not by running the native
|
||||
// iterator: `for...of` over it reads `next` and
|
||||
// @@iterator off prototypes the page can replace.
|
||||
// `length` is read every step, as the live iterator does
|
||||
const native = new client.native.NamedNodeMap(real(this));
|
||||
for (let i = 0; i < native.length; i++) yield native.item(i)!;
|
||||
return;
|
||||
}
|
||||
const nodes = attrs.nodes(owner);
|
||||
for (let i = 0; i < nodes.length; i++) yield nodes[i];
|
||||
for (const node of drain(nodes)) yield node;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,7 +169,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
ownKeys(target) {
|
||||
const names = attrs.names(element);
|
||||
const keys: (string | symbol)[] = [];
|
||||
const seen = new _Map<string, true>([]);
|
||||
const seen = new _Map<string, true>();
|
||||
|
||||
// an attribute named "0" is already listed as the index it shares
|
||||
// its key with, and a key listed twice breaks the proxy invariant
|
||||
@@ -174,18 +177,17 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
keys[keys.length] = String(i);
|
||||
seen.set(String(i), true);
|
||||
}
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
if (seen.get(names[i])) continue;
|
||||
seen.set(names[i], true);
|
||||
keys[keys.length] = names[i];
|
||||
for (const name of drain(names)) {
|
||||
if (seen.get(name)) continue;
|
||||
seen.set(name, true);
|
||||
keys[keys.length] = name;
|
||||
}
|
||||
// anything the native owns that is neither an index nor an
|
||||
// attribute name. there is nothing in practice, but a key that
|
||||
// exists and is left out of `ownKeys` is a proxy invariant
|
||||
// violation waiting to happen
|
||||
const own = Reflect_ownKeys(target);
|
||||
for (let i = 0; i < own.length; i++) {
|
||||
const key = own[i];
|
||||
for (const key of drain(own)) {
|
||||
if (typeof key !== "string") {
|
||||
keys[keys.length] = key;
|
||||
continue;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
_Map,
|
||||
_Set,
|
||||
_WeakMap,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
import { Arguments, Returns, Type, idlDOMString } from "@client/webidl";
|
||||
import { mirrorAttributeName } from "@client/attributes";
|
||||
@@ -207,8 +208,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
|
||||
const names = new _Set<string>();
|
||||
const own = Object_getOwnPropertyNames(style);
|
||||
for (let i = 0; i < own.length; i++) {
|
||||
if (!isIndex(own[i])) names.add(own[i]);
|
||||
for (const name of drain(own)) {
|
||||
if (!isIndex(name)) names.add(name);
|
||||
}
|
||||
cssAttributesByKind.set(kind, names);
|
||||
|
||||
@@ -384,11 +385,18 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
* element hands out writes that element's style attribute.
|
||||
*/
|
||||
if ("StylePropertyMap" in self) {
|
||||
const rewriteValues = (values: (CSSStyleValue | string)[]) => {
|
||||
const out: (CSSStyleValue | string)[] = [];
|
||||
// `(property, ...values)` as one array for `Reflect_apply`, since
|
||||
// spreading it into the call would run the page-replaceable iteration
|
||||
// protocol over values that were just rewritten
|
||||
const rewriteArgs = (
|
||||
property: string,
|
||||
values: (CSSStyleValue | string)[]
|
||||
) => {
|
||||
const out: (CSSStyleValue | string)[] = [property];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i];
|
||||
out[i] = typeof value === "string" && value ? rewrite(value) : value;
|
||||
out[i + 1] =
|
||||
typeof value === "string" && value ? rewrite(value) : value;
|
||||
}
|
||||
|
||||
return out;
|
||||
@@ -398,14 +406,14 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
@Arguments("USVString", "(CSSStyleValue or USVString)...")
|
||||
@Returns("undefined")
|
||||
set(property: string, ...values: (CSSStyleValue | string)[]): void {
|
||||
super.set(property, ...rewriteValues(values));
|
||||
Reflect_apply(super.set, this, rewriteArgs(property, values));
|
||||
touched(this);
|
||||
}
|
||||
|
||||
@Arguments("USVString", "(CSSStyleValue or USVString)...")
|
||||
@Returns("undefined")
|
||||
append(property: string, ...values: (CSSStyleValue | string)[]): void {
|
||||
super.append(property, ...rewriteValues(values));
|
||||
Reflect_apply(super.append, this, rewriteArgs(property, values));
|
||||
touched(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { rewriteHtml } from "@rewriters/html";
|
||||
import { ScramjetClient } from "@client/index";
|
||||
import { ForeignContext } from "@/shared/rewriters/html";
|
||||
import { Array_indexOf, String, String_substring } from "@/shared/snapshot";
|
||||
import {
|
||||
Array_indexOf,
|
||||
String,
|
||||
String_substring,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
import { Arguments, Returns, Type } from "@client/webidl";
|
||||
import { foreignContextForElement } from "@client/dom/markup";
|
||||
|
||||
@@ -144,8 +149,8 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
new client.native.Range(self).startContainer,
|
||||
new client.native.Range(self).endContainer,
|
||||
];
|
||||
for (let i = 0; i < ends.length; i++) {
|
||||
for (let n: Node | null = ends[i]; n && n !== common; n = parentOf(n)) {
|
||||
for (const end of drain(ends)) {
|
||||
for (let n: Node | null = end; n && n !== common; n = parentOf(n)) {
|
||||
if (!isText(n)) return true;
|
||||
}
|
||||
}
|
||||
@@ -278,7 +283,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
|
||||
const parts = covered(this, intersecting(this, root));
|
||||
let out = "";
|
||||
for (let i = 0; i < parts.length; i++) out += parts[i];
|
||||
for (const part of drain(parts)) out += part;
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -366,7 +371,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
text.record(nodes[i], String_substring(page, endOffset));
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < owners.length; i++) text.sync(owners[i]);
|
||||
for (const owner of drain(owners)) text.sync(owner);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
String,
|
||||
String_substring,
|
||||
String_toLowerCase,
|
||||
Reflect_apply,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
/** Node types, as `Node.prototype.nodeType` reports them. */
|
||||
@@ -257,13 +259,13 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
// back into the rewritten whole
|
||||
const self = this as Node;
|
||||
const affected = rawTextInclusiveDescendants(self);
|
||||
for (let i = 0; i < affected.length; i++) {
|
||||
text.normalizeChildren(affected[i]);
|
||||
for (const node of drain(affected)) {
|
||||
text.normalizeChildren(node);
|
||||
}
|
||||
|
||||
super.normalize();
|
||||
|
||||
for (let i = 0; i < affected.length; i++) text.sync(affected[i]);
|
||||
for (const node of drain(affected)) text.sync(node);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -276,7 +278,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
void super.hasAttributes();
|
||||
|
||||
const args = asNodes(this, nodes) as (Node | string)[];
|
||||
text.around(this, args, () => super.append(...args));
|
||||
text.around(this, args, () => Reflect_apply(super.append, this, args));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -285,7 +287,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
void super.hasAttributes();
|
||||
|
||||
const args = asNodes(this, nodes) as (Node | string)[];
|
||||
text.around(this, args, () => super.prepend(...args));
|
||||
text.around(this, args, () => Reflect_apply(super.prepend, this, args));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -294,7 +296,9 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
void super.hasAttributes();
|
||||
|
||||
const args = asNodes(this, nodes) as (Node | string)[];
|
||||
text.around(this, args, () => super.replaceChildren(...args));
|
||||
text.around(this, args, () =>
|
||||
Reflect_apply(super.replaceChildren, this, args)
|
||||
);
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -302,7 +306,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
after(...nodes: (Node | string)[]): void {
|
||||
const parent = super.parentNode;
|
||||
const args = asNodes(parent, nodes) as (Node | string)[];
|
||||
text.around(parent, args, () => super.after(...args));
|
||||
text.around(parent, args, () => Reflect_apply(super.after, this, args));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -310,7 +314,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
before(...nodes: (Node | string)[]): void {
|
||||
const parent = super.parentNode;
|
||||
const args = asNodes(parent, nodes) as (Node | string)[];
|
||||
text.around(parent, args, () => super.before(...args));
|
||||
text.around(parent, args, () => Reflect_apply(super.before, this, args));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -318,7 +322,9 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
replaceWith(...nodes: (Node | string)[]): void {
|
||||
const parent = super.parentNode;
|
||||
const args = asNodes(parent, nodes) as (Node | string)[];
|
||||
text.around(parent, args, () => super.replaceWith(...args));
|
||||
text.around(parent, args, () =>
|
||||
Reflect_apply(super.replaceWith, this, args)
|
||||
);
|
||||
}
|
||||
|
||||
@Arguments("DOMString", "DOMString")
|
||||
@@ -379,7 +385,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
append(...nodes: (Node | string)[]): void {
|
||||
void super.childElementCount;
|
||||
|
||||
text.around(this, nodes, () => super.append(...nodes));
|
||||
text.around(this, nodes, () => Reflect_apply(super.append, this, nodes));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -387,7 +393,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
prepend(...nodes: (Node | string)[]): void {
|
||||
void super.childElementCount;
|
||||
|
||||
text.around(this, nodes, () => super.prepend(...nodes));
|
||||
text.around(this, nodes, () => Reflect_apply(super.prepend, this, nodes));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -395,7 +401,9 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
replaceChildren(...nodes: (Node | string)[]): void {
|
||||
void super.childElementCount;
|
||||
|
||||
text.around(this, nodes, () => super.replaceChildren(...nodes));
|
||||
text.around(this, nodes, () =>
|
||||
Reflect_apply(super.replaceChildren, this, nodes)
|
||||
);
|
||||
}
|
||||
|
||||
@Arguments("Node", "Node?")
|
||||
@@ -413,7 +421,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
append(...nodes: (Node | string)[]): void {
|
||||
void super.childElementCount;
|
||||
|
||||
text.around(this, nodes, () => super.append(...nodes));
|
||||
text.around(this, nodes, () => Reflect_apply(super.append, this, nodes));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -421,7 +429,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
prepend(...nodes: (Node | string)[]): void {
|
||||
void super.childElementCount;
|
||||
|
||||
text.around(this, nodes, () => super.prepend(...nodes));
|
||||
text.around(this, nodes, () => Reflect_apply(super.prepend, this, nodes));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -429,7 +437,9 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
replaceChildren(...nodes: (Node | string)[]): void {
|
||||
void super.childElementCount;
|
||||
|
||||
text.around(this, nodes, () => super.replaceChildren(...nodes));
|
||||
text.around(this, nodes, () =>
|
||||
Reflect_apply(super.replaceChildren, this, nodes)
|
||||
);
|
||||
}
|
||||
|
||||
@Arguments("Node", "Node?")
|
||||
@@ -550,7 +560,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
after(...nodes: (Node | string)[]): void {
|
||||
const parent = super.parentNode;
|
||||
const args = asNodes(parent, nodes) as (Node | string)[];
|
||||
text.around(parent, args, () => super.after(...args));
|
||||
text.around(parent, args, () => Reflect_apply(super.after, this, args));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -558,7 +568,7 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
before(...nodes: (Node | string)[]): void {
|
||||
const parent = super.parentNode;
|
||||
const args = asNodes(parent, nodes) as (Node | string)[];
|
||||
text.around(parent, args, () => super.before(...args));
|
||||
text.around(parent, args, () => Reflect_apply(super.before, this, args));
|
||||
}
|
||||
|
||||
@Arguments("(Node or DOMString)...")
|
||||
@@ -566,7 +576,9 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
replaceWith(...nodes: (Node | string)[]): void {
|
||||
const parent = super.parentNode;
|
||||
const args = asNodes(parent, nodes) as (Node | string)[];
|
||||
text.around(parent, args, () => super.replaceWith(...args));
|
||||
text.around(parent, args, () =>
|
||||
Reflect_apply(super.replaceWith, this, args)
|
||||
);
|
||||
}
|
||||
|
||||
@Arguments()
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { ScramjetClient } from "@client/index";
|
||||
import { String, String_endsWith, String_startsWith } from "@/shared/snapshot";
|
||||
import {
|
||||
String,
|
||||
String_endsWith,
|
||||
String_startsWith,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
import { Arguments, Returns, Type } from "@client/webidl";
|
||||
|
||||
export default function (client: ScramjetClient) {
|
||||
@@ -37,8 +42,8 @@ export default function (client: ScramjetClient) {
|
||||
|
||||
const name = visibleName(nativeName(entry));
|
||||
const masked = client.config.maskedfiles;
|
||||
for (let i = 0; i < masked.length; i++) {
|
||||
if (String_endsWith(name, masked[i])) return true;
|
||||
for (const file of drain(masked)) {
|
||||
if (String_endsWith(name, file)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -26,7 +26,13 @@ import { ScramjetClient } from "@client/index";
|
||||
import { Arguments, Returns, Type, idlUSVString } from "@client/webidl";
|
||||
import { unrewriteUrl } from "@rewriters/url";
|
||||
import { XLINK_NAMESPACE } from "@client/attributes";
|
||||
import { String, String_startsWith, _URL } from "@/shared/snapshot";
|
||||
import {
|
||||
String,
|
||||
String_startsWith,
|
||||
_URL,
|
||||
Reflect_apply,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
export default function (client: ScramjetClient, self: Self) {
|
||||
const attrs = client.attributes;
|
||||
@@ -501,14 +507,14 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
@Arguments("DOMString...")
|
||||
@Returns("undefined")
|
||||
add(...tokens: string[]): void {
|
||||
super.add(...tokens);
|
||||
Reflect_apply(super.add, this, tokens);
|
||||
syncSandbox(this);
|
||||
}
|
||||
|
||||
@Arguments("DOMString...")
|
||||
@Returns("undefined")
|
||||
remove(...tokens: string[]): void {
|
||||
super.remove(...tokens);
|
||||
Reflect_apply(super.remove, this, tokens);
|
||||
syncSandbox(this);
|
||||
}
|
||||
|
||||
@@ -1378,7 +1384,7 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
"SVGFilterElement",
|
||||
];
|
||||
|
||||
for (const name of SVG_URI_REFERENCES) {
|
||||
for (const name of drain(SVG_URI_REFERENCES)) {
|
||||
client.Trap(`${name}.prototype.href`, {
|
||||
get(ctx) {
|
||||
const animated = ctx.get() as SVGAnimatedString;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
String_substring,
|
||||
_Set,
|
||||
_WeakMap,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
export const enabled = (_client: ScramjetClient, self: Self) =>
|
||||
@@ -57,8 +58,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
const all = Object_keys(area);
|
||||
const mine: string[] = [];
|
||||
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
if (String_startsWith(all[i], scope)) mine[mine.length] = all[i];
|
||||
for (const key of drain(all)) {
|
||||
if (String_startsWith(key, scope)) mine[mine.length] = key;
|
||||
}
|
||||
|
||||
return mine;
|
||||
@@ -139,7 +140,7 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
|
||||
// this site's entries only, never the whole area. `Object_keys`
|
||||
// snapshots, so removing while iterating is safe
|
||||
for (const key of scopedKeys(area)) {
|
||||
for (const key of drain(scopedKeys(area))) {
|
||||
nArea.removeItem(key);
|
||||
}
|
||||
}
|
||||
@@ -160,8 +161,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
proto = Object_getPrototypeOf(proto)
|
||||
) {
|
||||
const keys = Reflect_ownKeys(proto);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (typeof keys[i] === "string") memberNames.add(keys[i] as string);
|
||||
for (const key of drain(keys)) {
|
||||
if (typeof key === "string") memberNames.add(key as string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,8 +222,7 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
const keys: string[] = [];
|
||||
const own = Reflect_ownKeys(target);
|
||||
|
||||
for (let i = 0; i < own.length; i++) {
|
||||
const key = own[i];
|
||||
for (const key of drain(own)) {
|
||||
if (typeof key !== "string" || !String_startsWith(key, scope)) continue;
|
||||
keys[keys.length] = String_substring(key, scope.length);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
_URL,
|
||||
Object_defineProperty,
|
||||
Object_getOwnPropertyDescriptor,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
export function createLocationProxy(client: ScramjetClient, self: GlobalThis) {
|
||||
@@ -29,7 +30,7 @@ export function createLocationProxy(client: ScramjetClient, self: GlobalThis) {
|
||||
"port",
|
||||
"search",
|
||||
];
|
||||
for (const prop of urlprops) {
|
||||
for (const prop of drain(urlprops)) {
|
||||
const native = Object_getOwnPropertyDescriptor(descriptorSource, prop);
|
||||
if (!native) continue;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
String_trim,
|
||||
_Set,
|
||||
_URL,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
export const enabled = (_client: ScramjetClient, self: Self) =>
|
||||
@@ -41,8 +42,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
if (vary === null) return false;
|
||||
|
||||
const parts = String_split(vary, ",");
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (String_trim(parts[i]) === "*") return true;
|
||||
for (const part of drain(parts)) {
|
||||
if (String_trim(part) === "*") return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -135,8 +136,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
method: "add" | "addAll",
|
||||
put: (key: RequestInfo, response: Response) => Promise<void>
|
||||
): Promise<void> => {
|
||||
for (let i = 0; i < requests.length; i++) {
|
||||
const url = realUrl(requests[i]);
|
||||
for (const request of drain(requests)) {
|
||||
const url = realUrl(request);
|
||||
if (
|
||||
!String_startsWith(url, "http:") &&
|
||||
!String_startsWith(url, "https:")
|
||||
@@ -150,7 +151,7 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
|
||||
// a cache key is always a GET, and the spec refuses the batch here
|
||||
// rather than letting `put` reject it one entry in
|
||||
if (methodOf(requests[i]) !== "GET") {
|
||||
if (methodOf(request) !== "GET") {
|
||||
throw client.errors.typeError({
|
||||
execute: method,
|
||||
on: "Cache",
|
||||
@@ -171,8 +172,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
}
|
||||
const responses = await Promise_all(fetches);
|
||||
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
const status = new client.native.Response(responses[i]).status;
|
||||
for (const response of drain(responses)) {
|
||||
const status = new client.native.Response(response).status;
|
||||
|
||||
// `status` through the native rather than `ok`, which is a
|
||||
// page-replaceable accessor on `Response.prototype`. 206 is inside
|
||||
@@ -187,7 +188,7 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
});
|
||||
}
|
||||
|
||||
if (variesOnEverything(responses[i])) {
|
||||
if (variesOnEverything(response)) {
|
||||
throw client.errors.typeError({
|
||||
execute: method,
|
||||
on: "Cache",
|
||||
@@ -197,8 +198,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
}
|
||||
|
||||
const seen = new _Set<string>();
|
||||
for (let i = 0; i < requests.length; i++) {
|
||||
const url = realUrl(requests[i]);
|
||||
for (const request of drain(requests)) {
|
||||
const url = realUrl(request);
|
||||
if (seen.has(url)) {
|
||||
throw client.errors.domException("InvalidStateError", {
|
||||
execute: method,
|
||||
@@ -245,7 +246,7 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
request === undefined ? undefined : cacheKey(request),
|
||||
options
|
||||
);
|
||||
for (const match of matches) tag(match);
|
||||
for (const match of drain(matches)) tag(match);
|
||||
|
||||
return matches;
|
||||
}
|
||||
@@ -296,8 +297,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
: super.keys(cacheKey(request), options));
|
||||
const visible: Request[] = [];
|
||||
|
||||
for (let i = 0; i < stored.length; i++) {
|
||||
visible[visible.length] = pageKey(stored[i]);
|
||||
for (const key of drain(stored)) {
|
||||
visible[visible.length] = pageKey(key);
|
||||
}
|
||||
|
||||
return visible;
|
||||
@@ -383,13 +384,13 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
const names = await super.keys();
|
||||
const prefix = scopePrefix();
|
||||
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
if (!String_startsWith(names[i], prefix)) continue;
|
||||
for (const name of drain(names)) {
|
||||
if (!String_startsWith(name, prefix)) continue;
|
||||
|
||||
// sequential on purpose - the spec's answer is the first match
|
||||
// in insertion order, so a later cache must not be able to win
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const cache = await super.open(names[i]);
|
||||
const cache = await super.open(name);
|
||||
// through the native rather than the page-visible
|
||||
// `Cache.prototype.match`, which would run `cacheKey` a second
|
||||
// time over a key that has already been through it
|
||||
@@ -417,9 +418,9 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
const prefix = scopePrefix();
|
||||
const visible: string[] = [];
|
||||
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
if (String_startsWith(names[i], prefix)) {
|
||||
visible[visible.length] = String_substring(names[i], prefix.length);
|
||||
for (const name of drain(names)) {
|
||||
if (String_startsWith(name, prefix)) {
|
||||
visible[visible.length] = String_substring(name, prefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
|
||||
import { iswindow } from "@client/entry";
|
||||
import { ScramjetClient } from "@client/index";
|
||||
import { String_split } from "@/shared/snapshot";
|
||||
|
||||
// type self as any here, most of these are not defined in the types
|
||||
export default function (client: ScramjetClient, self: any) {
|
||||
const del = (name: string) => {
|
||||
const split = name.split(".");
|
||||
const prop = split.pop();
|
||||
const target = split.reduce((a, b) => a?.[b], self);
|
||||
if (!target) return;
|
||||
if (prop && prop in target) delete target[prop];
|
||||
const path = String_split(name, ".");
|
||||
const prop = path[path.length - 1];
|
||||
let target = self;
|
||||
for (let i = 0; i < path.length - 1; i++) target = target?.[path[i]];
|
||||
if (target && prop in target) delete target[prop];
|
||||
};
|
||||
|
||||
// obviously
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Object_getOwnPropertyDescriptor,
|
||||
Reflect_apply,
|
||||
Symbol_iterator,
|
||||
Array_includes,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
export const enabled = (client: ScramjetClient) =>
|
||||
@@ -23,8 +24,8 @@ export function argdbg(arg, recurse = []) {
|
||||
if (desc && desc.get) continue;
|
||||
|
||||
const ar = arg[prop];
|
||||
if (recurse.includes(ar)) continue;
|
||||
recurse.push(ar);
|
||||
if (Array_includes(recurse, ar)) continue;
|
||||
recurse[recurse.length] = ar;
|
||||
argdbg(ar, recurse);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
String_substring,
|
||||
_Map,
|
||||
_WeakMap,
|
||||
drain,
|
||||
Array_includes,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
export default function (client: ScramjetClient, self: Self) {
|
||||
@@ -459,8 +461,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
const ontargets = (): object[] => {
|
||||
const found: object[] = [self.self];
|
||||
|
||||
for (const name of Object_getOwnPropertyNames(self)) {
|
||||
if (synthetic.indexOf(name) !== -1) continue;
|
||||
for (const name of drain(Object_getOwnPropertyNames(self))) {
|
||||
if (Array_includes(synthetic, name)) continue;
|
||||
|
||||
const descriptor = Object_getOwnPropertyDescriptor(self, name);
|
||||
if (!descriptor || typeof descriptor.value !== "function") continue;
|
||||
@@ -474,9 +476,9 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
|
||||
const handlertypes = Object_keys(handlers);
|
||||
|
||||
for (const target of ontargets()) {
|
||||
for (let i = 0; i < handlertypes.length; i++) {
|
||||
const key = "on" + handlertypes[i];
|
||||
for (const target of drain(ontargets())) {
|
||||
for (const type of drain(handlertypes)) {
|
||||
const key = "on" + type;
|
||||
|
||||
const descriptor = Object_getOwnPropertyDescriptor(target, key);
|
||||
if (!descriptor || !descriptor.get || !descriptor.set) continue;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
String_indexOf,
|
||||
String_startsWith,
|
||||
String_substring,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
export const enabled = (_client: ScramjetClient, self: Self) =>
|
||||
@@ -51,15 +52,15 @@ export default function (client: ScramjetClient) {
|
||||
const prefix = `${client.scopeOrigin}@`;
|
||||
const visible: IDBDatabaseInfo[] = [];
|
||||
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
const name = all[i].name;
|
||||
for (const database of drain(all)) {
|
||||
const name = database.name;
|
||||
// a database with no name is not one of ours and cannot be
|
||||
// attributed to this origin
|
||||
if (name === undefined || !String_startsWith(name, prefix)) continue;
|
||||
|
||||
visible[visible.length] = {
|
||||
name: String_substring(name, prefix.length),
|
||||
version: all[i].version,
|
||||
version: database.version,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -227,37 +227,6 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
new client.native.EventTarget(self).addEventListener("message", gate, true);
|
||||
}
|
||||
|
||||
// if (iswindow)
|
||||
// client.Proxy("window.postMessage", {
|
||||
// apply(ctx) {
|
||||
// // console.log(
|
||||
// // callerClient,
|
||||
// // client,
|
||||
// // callerGlobalThisProxied.document,
|
||||
// // self.document,
|
||||
// // callerClient === client
|
||||
// // );
|
||||
// const inherit =
|
||||
// callerClient.url.href === "about:srcdoc" ||
|
||||
// callerClient.url.href === "about:blank";
|
||||
// ctx.args[0] = {
|
||||
// $scramjet$messagetype: "window",
|
||||
// $scramjet$origin: inherit
|
||||
// ? callerClient.global.parent[SCRAMJETCLIENT].url.origin
|
||||
// : callerClient.url.origin,
|
||||
// $scramjet$data: ctx.args[0],
|
||||
// };
|
||||
// // console.error("?", ctx.args);
|
||||
// // eval("debugger");
|
||||
|
||||
// // * origin because obviously
|
||||
// if (typeof ctx.args[1] === "string") ctx.args[1] = "*";
|
||||
// if (typeof ctx.args[1] === "object") ctx.args[1].targetOrigin = "*";
|
||||
|
||||
// ctx.return(wrappedPostMessage.call(ctx.fn, ...ctx.args));
|
||||
// },
|
||||
// });
|
||||
|
||||
client.Proxy("BroadcastChannel.prototype.postMessage", {
|
||||
apply(ctx) {
|
||||
ctx.args[0] = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
String_startsWith,
|
||||
String_substring,
|
||||
String_toLowerCase,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
import { carriedHeaderName, uncarriedHeaderName } from "@/shared/headers";
|
||||
import { ScramjetClient } from "@client/client";
|
||||
@@ -74,20 +75,18 @@ export default function (client: ScramjetClient) {
|
||||
const restored: { name: string; line: string }[] = [];
|
||||
const lines = String_split(raw, "\r\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const colon = String_indexOf(lines[i], ":");
|
||||
for (const line of drain(lines)) {
|
||||
const colon = String_indexOf(line, ":");
|
||||
if (colon === -1) continue;
|
||||
|
||||
const carried = uncarriedHeaderName(
|
||||
String_substring(lines[i], 0, colon)
|
||||
);
|
||||
const carried = uncarriedHeaderName(String_substring(line, 0, colon));
|
||||
if (carried === null) continue;
|
||||
|
||||
const name = String_toLowerCase(carried);
|
||||
// the value keeps the separator and its leading space verbatim
|
||||
restored[restored.length] = {
|
||||
name,
|
||||
line: name + String_substring(lines[i], colon),
|
||||
line: name + String_substring(line, colon),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Reflect_apply,
|
||||
Reflect_get,
|
||||
String_startsWith,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
|
||||
/** The init members a string-input `fetch()` / `new Request()` reads, sorted. */
|
||||
@@ -42,12 +43,23 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
|
||||
const toNativeHeaders = (headers: Headers) => {
|
||||
const nGlobal = new client.native.window(self);
|
||||
const nHeaders = new client.native.Headers(headers);
|
||||
const newHeaders = new nGlobal.Headers();
|
||||
for (const [key, value] of nHeaders.entries()) {
|
||||
const nNew = new client.native.Headers(newHeaders);
|
||||
|
||||
// `forEach`, which the native drives itself, rather than
|
||||
// `for (const [key, value] of nHeaders.entries())`. Reaching the
|
||||
// headers through `client.native` bought nothing there, because the
|
||||
// iteration never touched the native: the loop read @@iterator off the
|
||||
// iterator, which resolves to the page-replaceable `%IteratorPrototype%`,
|
||||
// and the destructuring read `Array.prototype[@@iterator]`. A page could
|
||||
// therefore choose what the restored view contained - and that view is
|
||||
// what `new Headers(init)`, `new Request(url, {headers})` and
|
||||
// `new Response(body, {headers})` are filled from
|
||||
new client.native.Headers(headers).forEach((value: string, key: string) => {
|
||||
const original = uncarriedHeaderName(key);
|
||||
if (original !== null) newHeaders.set(original, value);
|
||||
}
|
||||
if (original !== null) nNew.set(original, value);
|
||||
});
|
||||
|
||||
return newHeaders;
|
||||
};
|
||||
|
||||
@@ -100,8 +112,8 @@ export default function (client: ScramjetClient, self: Self) {
|
||||
return { init, members };
|
||||
}
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
members[keys[i]] = (init as Record<string, unknown>)[keys[i]];
|
||||
for (const key of drain(keys)) {
|
||||
members[key] = (init as Record<string, unknown>)[key];
|
||||
}
|
||||
|
||||
if (members.mode !== undefined) members.mode = idlDOMString(members.mode);
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { rewriteJs } from "@rewriters/js";
|
||||
import { GlobalScope, ScramjetClient } from "@client/index";
|
||||
import { String, TextDecoder_decode } from "@/shared/snapshot";
|
||||
import { String, TextDecoder_decode, Reflect_apply } from "@/shared/snapshot";
|
||||
import { Arguments, Returns } from "@client/webidl";
|
||||
|
||||
export default function (client: ScramjetClient, _self: Self) {
|
||||
/**
|
||||
* `(handler, timeout, ...args)` as one array for `Reflect_apply`. Spreading
|
||||
* `args` into the call would run the page-replaceable iteration protocol,
|
||||
* so the timer need not be called with what the coercion approved.
|
||||
*/
|
||||
const timerArguments = (
|
||||
handler: TimerHandler,
|
||||
timeout: number | undefined,
|
||||
args: any[]
|
||||
) => {
|
||||
const call: any[] = [handler, timeout];
|
||||
for (let i = 0; i < args.length; i++) call[i + 2] = args[i];
|
||||
|
||||
return call;
|
||||
};
|
||||
|
||||
const rewriteHandler = (handler: TimerHandler): TimerHandler => {
|
||||
if (typeof handler === "function") return handler;
|
||||
|
||||
@@ -35,10 +51,10 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
// receiver silently made `setTimeout.call({}, fn, 0)` work; and a
|
||||
// bare call passes undefined, which WebIDL sends to the global for
|
||||
// a member of a [Global] interface, so that still resolves
|
||||
return new client.native.window(this).setTimeout(
|
||||
rewriteHandler(handler),
|
||||
timeout,
|
||||
...args
|
||||
return Reflect_apply(
|
||||
new client.native.window(this).setTimeout,
|
||||
null,
|
||||
timerArguments(rewriteHandler(handler), timeout, args)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,10 +65,10 @@ export default function (client: ScramjetClient, _self: Self) {
|
||||
timeout?: number,
|
||||
...args: any[]
|
||||
): number {
|
||||
return new client.native.window(this).setInterval(
|
||||
rewriteHandler(handler),
|
||||
timeout,
|
||||
...args
|
||||
return Reflect_apply(
|
||||
new client.native.window(this).setInterval,
|
||||
null,
|
||||
timerArguments(rewriteHandler(handler), timeout, args)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Number_isSafeInteger,
|
||||
Error,
|
||||
TextDecoder_decode,
|
||||
Reflect_apply,
|
||||
} from "@/shared/snapshot";
|
||||
import { base64ToBytes } from "@/shared/util";
|
||||
import { ProxyCtx, ScramjetClient } from "@client/index";
|
||||
@@ -123,11 +124,15 @@ function doUnrewrite(
|
||||
client: ScramjetClient,
|
||||
ctx: ProxyCtx<"Function.prototype.toString", "apply">
|
||||
) {
|
||||
const stringified: string = ctx.fn.call(ctx.this);
|
||||
// not `ctx.fn.call`, which looks `call` up on the page-writable
|
||||
// Function.prototype
|
||||
const stringified: string = Reflect_apply(ctx.fn, ctx.this, []);
|
||||
|
||||
const extracted = extractTag(stringified);
|
||||
if (!extracted) return ctx.return(stringified);
|
||||
const [tag, tagOffset, tagStart] = extracted;
|
||||
const tag = extracted[0];
|
||||
const tagOffset = extracted[1];
|
||||
const tagStart = extracted[2];
|
||||
|
||||
const fnStart = tagStart - tagOffset;
|
||||
const fnEnd = fnStart + stringified.length;
|
||||
@@ -151,12 +156,13 @@ function doUnrewrite(
|
||||
if (getEnd(rewrites[end]) < fnEnd) end++;
|
||||
else break;
|
||||
}
|
||||
const fnrewrites = rewrites.slice(i, end);
|
||||
|
||||
let newString = "";
|
||||
let lastpos = 0;
|
||||
|
||||
for (const rewrite of fnrewrites) {
|
||||
// indexed over the range rather than a `slice` to `for...of`: this runs
|
||||
// for every `toString` of a rewritten function
|
||||
for (; i < end; i++) {
|
||||
const rewrite = rewrites[i];
|
||||
newString += stringified.slice(lastpos, rewrite.start - fnStart);
|
||||
|
||||
if (rewrite.type === RewriteType.Insert) {
|
||||
|
||||
@@ -10,24 +10,25 @@ import {
|
||||
_WeakSet,
|
||||
Object_create,
|
||||
Function_hasInstance,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
import { FakeWebSocketState } from "./shared/requests/WebSocket";
|
||||
import { FakeWebSocketStreamState } from "./shared/requests/WebSocketStream";
|
||||
|
||||
export class SingletonBox {
|
||||
clients: ScramjetClient[] = [];
|
||||
clientIds: _Map<string, ScramjetClient> = new _Map([]);
|
||||
globals: _Map<Self, ScramjetClient> = new _Map([]);
|
||||
documents: _Map<Document, ScramjetClient> = new _Map([]);
|
||||
histories: _Map<History, ScramjetClient> = new _Map([]);
|
||||
objectPrototypes: _Map<object, ScramjetClient> = new _Map([]);
|
||||
locations: _Map<Location, ScramjetClient> = new _Map([]);
|
||||
functions: _Map<typeof Function, ScramjetClient> = new _Map([]);
|
||||
clientIds: _Map<string, ScramjetClient> = new _Map();
|
||||
globals: _Map<Self, ScramjetClient> = new _Map();
|
||||
documents: _Map<Document, ScramjetClient> = new _Map();
|
||||
histories: _Map<History, ScramjetClient> = new _Map();
|
||||
objectPrototypes: _Map<object, ScramjetClient> = new _Map();
|
||||
locations: _Map<Location, ScramjetClient> = new _Map();
|
||||
functions: _Map<typeof Function, ScramjetClient> = new _Map();
|
||||
writeRewriters: _WeakMap<Document, IncrementalHtmlRewriter> = new _WeakMap(
|
||||
[]
|
||||
);
|
||||
taggedHeaders: _WeakSet<Headers> = new _WeakSet([]);
|
||||
taggedResponses: _WeakSet<Response> = new _WeakSet([]);
|
||||
taggedHeaders: _WeakSet<Headers> = new _WeakSet();
|
||||
taggedResponses: _WeakSet<Response> = new _WeakSet();
|
||||
scopedOpfsRoots: _WeakSet<FileSystemHandle> = new _WeakSet();
|
||||
styleDeclarations: _WeakMap<CSSStyleDeclaration, CSSStyleDeclaration> =
|
||||
new _WeakMap();
|
||||
@@ -131,9 +132,9 @@ export class SingletonBox {
|
||||
|
||||
unproxy: _WeakMap<object, any> = new _WeakMap();
|
||||
|
||||
socketmap: _WeakMap<WebSocket, FakeWebSocketState> = new _WeakMap([]);
|
||||
socketmap: _WeakMap<WebSocket, FakeWebSocketState> = new _WeakMap();
|
||||
socketstreammap: _WeakMap<WebSocketStream, FakeWebSocketStreamState> =
|
||||
new _WeakMap([]);
|
||||
new _WeakMap();
|
||||
|
||||
ctors: Record<string, ((...args: any[]) => any)[]> = Object_create(null);
|
||||
|
||||
@@ -185,8 +186,7 @@ export class SingletonBox {
|
||||
this.clientIds.set(client.id, client);
|
||||
|
||||
const names = Object_getOwnPropertyNames(global);
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const prop = names[i];
|
||||
for (const prop of drain(names)) {
|
||||
const desc = Object_getOwnPropertyDescriptor(global, prop);
|
||||
if (desc && typeof desc.value === "function") {
|
||||
let ctors = this.ctors[prop];
|
||||
@@ -208,8 +208,11 @@ export class SingletonBox {
|
||||
// not `instanceof`, which would run a page-defined
|
||||
// `Symbol.hasInstance` - and callers use the answer to decide whether a
|
||||
// value gets rewritten
|
||||
for (const ctor of ctors) {
|
||||
if (Function_hasInstance(ctor, obj)) return true;
|
||||
// indexed rather than `for...of` or `drain`: this is the brand check
|
||||
// every interceptor gates on, the hottest path in the client, and the
|
||||
// bare protocol would have let a page decide its answer
|
||||
for (let i = 0; i < ctors.length; i++) {
|
||||
if (Function_hasInstance(ctors[i], obj)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ import {
|
||||
Symbol_iterator,
|
||||
TypeError,
|
||||
Error,
|
||||
Array_includes,
|
||||
drain,
|
||||
} from "@/shared/snapshot";
|
||||
import type { AnyFunction } from "@/types";
|
||||
|
||||
@@ -680,7 +682,7 @@ export type IDLMemberSignature = {
|
||||
|
||||
// keyed on the function object rather than `context.metadata`, which swc
|
||||
// doesn't implement for 2022-03 decorators
|
||||
const signatures = new _WeakMap([]) as WeakMap<AnyFunction, IDLMemberSignature>;
|
||||
const signatures = new _WeakMap() as WeakMap<AnyFunction, IDLMemberSignature>;
|
||||
|
||||
function record(fn: AnyFunction, patch: IDLMemberSignature) {
|
||||
const existing = signatures.get(fn);
|
||||
@@ -1175,11 +1177,11 @@ function compileIDLUnion(box: IDLBrandChecker, inner: string): IDLCoerce {
|
||||
const interfaces: string[] = [];
|
||||
let fallback: IDLCoerce | undefined;
|
||||
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const raw = members[i].trim();
|
||||
for (const member of drain(members)) {
|
||||
const raw = member.trim();
|
||||
const name = stripIDLExtendedAttributes(raw);
|
||||
|
||||
if (IDL_STRING_TYPES.indexOf(name) !== -1) {
|
||||
if (Array_includes(IDL_STRING_TYPES, name)) {
|
||||
// two string members can't be told apart, so give up
|
||||
if (fallback) return idlPassthrough;
|
||||
fallback = compileIDLType(box, raw);
|
||||
@@ -1194,7 +1196,7 @@ function compileIDLUnion(box: IDLBrandChecker, inner: string): IDLCoerce {
|
||||
// an interface this realm does not have at all: nothing can be an
|
||||
// instance of it, so the member is dropped and the rest of the union
|
||||
// still gets converted
|
||||
if (IDL_OPTIONAL_INTERFACES.indexOf(name) !== -1) continue;
|
||||
if (Array_includes(IDL_OPTIONAL_INTERFACES, name)) continue;
|
||||
|
||||
// a dictionary, enum, sequence, numeric or unknown member — the spec's
|
||||
// disambiguation rules for those need more type information than we have
|
||||
@@ -1222,7 +1224,7 @@ function compileIDLUnion(box: IDLBrandChecker, inner: string): IDLCoerce {
|
||||
* name boundary, and `"DOMString..."` has no name at all.
|
||||
*/
|
||||
function stripIDLArgumentName(s: string): string {
|
||||
if (IDL_MULTIWORD_PRIMITIVES.indexOf(s) !== -1) return s;
|
||||
if (Array_includes(IDL_MULTIWORD_PRIMITIVES, s)) return s;
|
||||
|
||||
const space = s.lastIndexOf(" ");
|
||||
if (space <= 0) return s;
|
||||
@@ -1367,7 +1369,7 @@ function compileIDLType(
|
||||
if (enforceRange) return idlPassthrough;
|
||||
|
||||
// the extended attribute is only ever spec'd on string types
|
||||
if (nullToEmpty && IDL_STRING_TYPES.indexOf(s) !== -1) {
|
||||
if (nullToEmpty && Array_includes(IDL_STRING_TYPES, s)) {
|
||||
return (value) => (value === null ? "" : primitive(value));
|
||||
}
|
||||
|
||||
@@ -1881,12 +1883,11 @@ export function dictionaryReader<
|
||||
): (value: unknown) => IDLDictionaryOf<P & M> {
|
||||
const keys = sortedIDLMemberNames(inherited ?? {});
|
||||
const own = sortedIDLMemberNames(members);
|
||||
for (let i = 0; i < own.length; i++) keys[keys.length] = own[i];
|
||||
for (const key of drain(own)) keys[keys.length] = key;
|
||||
const all: Record<string, string> = { ...inherited, ...members };
|
||||
|
||||
const compiled: CompiledMember[] = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
for (const key of drain(keys)) {
|
||||
const raw = String_trim(all[key]);
|
||||
|
||||
let declaration = raw;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GlobalScope, ScramjetClient } from "@client/index";
|
||||
import { String } from "@/shared/snapshot";
|
||||
import { String, Reflect_apply } from "@/shared/snapshot";
|
||||
import { Arguments, Returns, idlUSVString } from "@client/webidl";
|
||||
|
||||
export default function (client: ScramjetClient) {
|
||||
@@ -48,7 +48,14 @@ export default function (client: ScramjetClient) {
|
||||
// fixed where the failure is produced rather than where it surfaces.
|
||||
// (The SyntaxError for an unparseable URL is already clean: such a
|
||||
// URL reaches the native exactly as the page wrote it.)
|
||||
new client.native.window(this).importScripts(...rewritten);
|
||||
// `Reflect_apply` rather than a spread: the spread would run the
|
||||
// page-replaceable iteration protocol over the list just rewritten,
|
||||
// so a page could hand the native other URLs than `rewriteUrl` saw
|
||||
Reflect_apply(
|
||||
new client.native.window(this).importScripts,
|
||||
null,
|
||||
rewritten
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -89,7 +89,11 @@ export const BigInt_asUintN = globalThis.BigInt.asUintN;
|
||||
|
||||
export const encodeURIComponent = globalThis.encodeURIComponent;
|
||||
|
||||
export const Symbol_iterator = globalThis.Symbol.iterator;
|
||||
// annotated rather than inferred: an alias widens `unique symbol` to plain
|
||||
// `symbol`, and a computed key of that type builds an index signature instead
|
||||
// of the well-known `[Symbol.iterator]` member `Iterable<T>` asks for
|
||||
export const Symbol_iterator: typeof Symbol.iterator =
|
||||
globalThis.Symbol.iterator;
|
||||
/**
|
||||
* `OrdinaryHasInstance`, as `Function.prototype[Symbol.hasInstance]` exposes
|
||||
* it. Called directly rather than through `instanceof`, which consults the
|
||||
@@ -208,6 +212,70 @@ export const Array_indexOf = Function_prototype_call.bind(
|
||||
from?: number
|
||||
) => number;
|
||||
|
||||
/**
|
||||
* `filter` and `map` rather than the methods, for the same reason everything
|
||||
* else here is captured: the page can replace `Array.prototype.filter`. They
|
||||
* are also the array operations that do *not* run the iteration protocol -
|
||||
* unlike `for...of`, destructuring and spread, which reach
|
||||
* `%IteratorPrototype%[@@iterator]` and `Array.prototype[@@iterator]`, both of
|
||||
* which are writable and configurable. See `no-unsafe-iteration`.
|
||||
*/
|
||||
export const Array_filter = Function_prototype_call.bind(
|
||||
globalThis.Array.prototype.filter
|
||||
) as <T>(array: T[], predicate: (value: T, index: number) => unknown) => T[];
|
||||
export const Array_map = Function_prototype_call.bind(
|
||||
globalThis.Array.prototype.map
|
||||
) as <T, U>(array: T[], transform: (value: T, index: number) => U) => U[];
|
||||
|
||||
/**
|
||||
* A `for...of`-able view of `items` that a page cannot hijack.
|
||||
*
|
||||
* `for...of`, array destructuring and spread all run the iteration protocol,
|
||||
* and every lookup in it lands somewhere a page can write: `@@iterator` on
|
||||
* `Array.prototype` and on `%IteratorPrototype%`, `next` on the iterator's own
|
||||
* prototype, and `done`/`value` whenever the result object inherits them.
|
||||
* Reaching the *value* through `client.native` does not help, because the
|
||||
* iteration never touches the native.
|
||||
*
|
||||
* This hands back a protocol built entirely from own properties of objects
|
||||
* created right here, so each of those lookups resolves before it reaches
|
||||
* anything the page can reassign. `for (const x of drain(xs))` is then exactly
|
||||
* as safe as an indexed loop, and reads like the loop it replaces.
|
||||
*
|
||||
* `items` has to be something already safe to index: an array this code built
|
||||
* - `Object_keys`, `Reflect_ownKeys`, a literal, a native's return value - and
|
||||
* not a live platform collection, whose `length` is a page-replaceable
|
||||
* accessor in its own right.
|
||||
*
|
||||
* Costs roughly ten times an indexed loop per element. That is nothing at the
|
||||
* sizes it is used on here, and it is the one reason `SingletonBox.instanceof`
|
||||
* stays indexed.
|
||||
*/
|
||||
export function drain<T>(items: ArrayLike<T>): Iterable<T> {
|
||||
return {
|
||||
[Symbol_iterator](): Iterator<T> {
|
||||
let i = 0;
|
||||
// one result object, reused. The protocol reads `done` and `value`
|
||||
// off it before asking for the next step, so a fresh one per step
|
||||
// buys nothing the engine does not already elide
|
||||
const step = { done: false, value: undefined as T };
|
||||
|
||||
return {
|
||||
next() {
|
||||
if (i < items.length) {
|
||||
step.value = items[i++];
|
||||
} else {
|
||||
step.done = true;
|
||||
step.value = undefined as T;
|
||||
}
|
||||
|
||||
return step as IteratorResult<T>;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const JSON_parse = globalThis.JSON.parse;
|
||||
export const JSON_stringify = globalThis.JSON.stringify;
|
||||
|
||||
@@ -244,22 +312,51 @@ export const Math_floor = globalThis.Math.floor;
|
||||
export const Math_trunc = globalThis.Math.trunc;
|
||||
export const Math_fround = globalThis.Math.fround;
|
||||
|
||||
export const Promise_all = globalThis.Promise.all.bind(globalThis.Promise);
|
||||
export const Promise_race = globalThis.Promise.race.bind(globalThis.Promise);
|
||||
const _Promise = globalThis.Promise;
|
||||
export const Promise_resolve = globalThis.Promise.resolve.bind(
|
||||
globalThis.Promise
|
||||
);
|
||||
export const Promise_reject = globalThis.Promise.reject.bind(
|
||||
globalThis.Promise
|
||||
);
|
||||
export const Promise_allSettled = globalThis.Promise.allSettled.bind(
|
||||
globalThis.Promise
|
||||
);
|
||||
export const Promise_any = globalThis.Promise.any.bind(globalThis.Promise);
|
||||
export const Promise_then = Function_prototype_call.bind(
|
||||
globalThis.Promise.prototype.then
|
||||
);
|
||||
|
||||
/**
|
||||
* `Promise.all`, without the parts of it a page can reach.
|
||||
*
|
||||
* Binding the native only fixes which `all` runs. It still walks its argument
|
||||
* with the iteration protocol, through the page-replaceable
|
||||
* `Array.prototype[@@iterator]`, and settles each element by calling its
|
||||
* `then` - `Promise.prototype.then`, also page-writable. Either would let a
|
||||
* page choose what an awaited batch resolved to. This indexes the array and
|
||||
* settles each element through the snapshotted `then`.
|
||||
*
|
||||
* `items` has to be safe to index, as for `drain`.
|
||||
*/
|
||||
export function Promise_all<T>(
|
||||
items: ArrayLike<T | PromiseLike<T>>
|
||||
): Promise<Awaited<T>[]> {
|
||||
return new _Promise((resolve, reject) => {
|
||||
const results: Awaited<T>[] = [];
|
||||
let remaining = items.length;
|
||||
if (remaining === 0) return resolve(results);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const index = i;
|
||||
Promise_then(
|
||||
Promise_resolve(items[i]),
|
||||
(value: Awaited<T>) => {
|
||||
results[index] = value;
|
||||
if (--remaining === 0) resolve(results);
|
||||
},
|
||||
reject
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const Symbol_for = globalThis.Symbol.for;
|
||||
|
||||
declare const WrappedBrand: unique symbol;
|
||||
|
||||
@@ -311,9 +311,18 @@ const brandCheckPlugin = {
|
||||
"CallExpression:exit"(node) {
|
||||
const frame = frames[frames.length - 1];
|
||||
if (!frame?.member) return;
|
||||
const callee = node.callee;
|
||||
// `Reflect_apply(super.m, this, args)` is `super.m(...args)`
|
||||
// without the spread, which runs the page-replaceable
|
||||
// iteration protocol - see `no-unsafe-iteration`
|
||||
const applied =
|
||||
node.callee.type === "Identifier" &&
|
||||
node.callee.name === "Reflect_apply" &&
|
||||
node.arguments.length >= 2 &&
|
||||
node.arguments[1].type === "ThisExpression" &&
|
||||
node.arguments[0].type === "MemberExpression";
|
||||
const callee = applied ? node.arguments[0] : node.callee;
|
||||
if (
|
||||
node.optional ||
|
||||
(!applied && node.optional) ||
|
||||
callee.type !== "MemberExpression" ||
|
||||
callee.optional ||
|
||||
callee.computed
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* The iteration protocol runs through two intrinsics a page can replace:
|
||||
*
|
||||
* %IteratorPrototype%[Symbol.iterator] writable, configurable
|
||||
* Array.prototype[Symbol.iterator] writable, configurable
|
||||
*
|
||||
* `for...of` over a *native iterator* - `headers.entries()`, `map.values()` -
|
||||
* looks `@@iterator` up on the iterator, which resolves to
|
||||
* `%IteratorPrototype%`, so replacing it hijacks the loop. Array destructuring
|
||||
* and spread go through `Array.prototype[@@iterator]` the same way, and that
|
||||
* holds even for an array scramjet built itself a line earlier.
|
||||
*
|
||||
* So any of these, running while page script can have executed, hands the page
|
||||
* control of what scramjet believes it read. Reaching the object through
|
||||
* `client.native` does not help: the *iteration* never touches the native.
|
||||
*
|
||||
* The sanctioned replacement is `for (const x of drain(xs))`. `drain` hands
|
||||
* back a protocol made of own properties on objects it creates, so none of
|
||||
* those lookups reach anything the page can write, and this rule accepts it.
|
||||
* The alternatives are an indexed `for (let i = 0; ...)` loop, the snapshotted
|
||||
* `Array_filter` / `Array_map` / `Array_join`, or a callback enumeration the
|
||||
* native drives itself (`Headers.prototype.forEach` through `client.native`).
|
||||
*
|
||||
* Array destructuring and spread are *not* excused by `drain` - they run the
|
||||
* protocol on the element, not on the collection - so `for (const [k, v] of
|
||||
* drain(...))` still reports. Destructure an object, or index the entry.
|
||||
*
|
||||
* Install-time code is not special-cased. It is safe on pristine intrinsics,
|
||||
* but `drain` costs nothing that matters at install, and an exemption would
|
||||
* have to guess at which functions run before page script - `saveNatives`,
|
||||
* `hook` and `Intercept` are class methods, not the module's default export,
|
||||
* so the obvious "top-level default export" heuristic would miss most of them.
|
||||
*/
|
||||
|
||||
const PROTOCOL_FORMS = {
|
||||
ForOfStatement: "forOf",
|
||||
ArrayPattern: "destructuring",
|
||||
SpreadElement: "spread",
|
||||
};
|
||||
|
||||
const noUnsafeIterationPlugin = {
|
||||
rules: {
|
||||
"no-unsafe-iteration": {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"disallow the iteration protocol on values a page can reach",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
forOf:
|
||||
"`for...of` reads @@iterator off the value, which for a native iterator resolves to the page-replaceable %IteratorPrototype%. Use `for (const x of drain(xs))` from the snapshot, an indexed loop on a hot path, or a native `forEach` through `client.native`.",
|
||||
destructuring:
|
||||
"Array destructuring reads the page-replaceable `Array.prototype[Symbol.iterator]`. Index the value instead.",
|
||||
spread:
|
||||
"Spread reads @@iterator off the value, which the page can replace. Pass an argument list with `Reflect_apply(fn, this, args)`, or build the array by index.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
// `drain` has to be the one from the snapshot. A file that never
|
||||
// imports it cannot be calling it, whatever a local is named
|
||||
let drainImported = false;
|
||||
|
||||
function isDrained(node) {
|
||||
const right = node.right;
|
||||
|
||||
return (
|
||||
drainImported &&
|
||||
right?.type === "CallExpression" &&
|
||||
right.callee.type === "Identifier" &&
|
||||
right.callee.name === "drain"
|
||||
);
|
||||
}
|
||||
|
||||
function report(node, messageId) {
|
||||
context.report({ node, messageId });
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source.value !== "@/shared/snapshot") return;
|
||||
|
||||
for (const specifier of node.specifiers) {
|
||||
if (
|
||||
specifier.type === "ImportSpecifier" &&
|
||||
specifier.imported.name === "drain"
|
||||
) {
|
||||
drainImported = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
ForOfStatement(node) {
|
||||
if (isDrained(node)) return;
|
||||
|
||||
report(node, PROTOCOL_FORMS.ForOfStatement);
|
||||
},
|
||||
ArrayPattern(node) {
|
||||
report(node, PROTOCOL_FORMS.ArrayPattern);
|
||||
},
|
||||
SpreadElement(node) {
|
||||
// `{...obj}` is an object spread, which copies own
|
||||
// enumerable properties and never touches @@iterator
|
||||
if (node.parent?.type === "ObjectExpression") return;
|
||||
|
||||
report(node, PROTOCOL_FORMS.SpreadElement);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default noUnsafeIterationPlugin;
|
||||
@@ -27,6 +27,7 @@ for (const body of [
|
||||
"void super.toString(); return 1;",
|
||||
"new client.native.Blob(this).size = 1; return 1;",
|
||||
"void new client.native.Node(this).ELEMENT_NODE; return 1;",
|
||||
"Reflect_apply(super.slice, other, args); return 1;",
|
||||
]) {
|
||||
test(`rejects unchecked path: ${body}`, () => {
|
||||
const messages = check(body);
|
||||
@@ -42,6 +43,7 @@ for (const body of [
|
||||
"void new client.native.Blob(this).size; return 1;",
|
||||
"if (flag) return super.size; super.slice(); return 1;",
|
||||
"try { return super.size; } catch { throw Error(); }",
|
||||
"Reflect_apply(super.slice, this, args); return 1;",
|
||||
]) {
|
||||
test(`accepts native check: ${body}`, () =>
|
||||
assert.deepEqual(check(body), []));
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { Linter } from "eslint";
|
||||
import plugin from "../eslint/no-unsafe-iteration-plugin.mjs";
|
||||
|
||||
const linter = new Linter();
|
||||
function check(code) {
|
||||
return linter.verify(code, [
|
||||
{
|
||||
plugins: { local: plugin },
|
||||
rules: { "local/no-unsafe-iteration": "error" },
|
||||
languageOptions: { ecmaVersion: 2022, sourceType: "module" },
|
||||
},
|
||||
]);
|
||||
}
|
||||
const DRAIN = 'import { drain } from "@/shared/snapshot";\n';
|
||||
|
||||
for (const code of [
|
||||
"for (const x of xs) f(x);",
|
||||
"const [a, b] = pair;",
|
||||
"f(...args);",
|
||||
"const copy = [...xs];",
|
||||
// a local called drain is not the snapshot's
|
||||
"const drain = (x) => x; for (const x of drain(xs)) f(x);",
|
||||
// drain covers the collection, not destructuring of each element
|
||||
DRAIN + "for (const [k, v] of drain(entries)) f(k, v);",
|
||||
]) {
|
||||
test(`rejects: ${code}`, () => assert.ok(check(code).length > 0));
|
||||
}
|
||||
for (const code of [
|
||||
DRAIN + "for (const x of drain(xs)) f(x);",
|
||||
"for (let i = 0; i < xs.length; i++) f(xs[i]);",
|
||||
"const copy = { ...obj };",
|
||||
"const { a, b } = obj;",
|
||||
]) {
|
||||
test(`accepts: ${code}`, () => assert.deepEqual(check(code), []));
|
||||
}
|
||||
Reference in new issue
Block a user