-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjavascript-source.js
More file actions
200 lines (200 loc) · 12.3 KB
/
Copy pathjavascript-source.js
File metadata and controls
200 lines (200 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
(() => {
"use strict";
// References never leave this page. Paths are labels and rediscovery hints only.
const root = globalThis;
const excluded = new Set([
"window", "self", "top", "parent", "frames", "globalThis", "document", "location", "navigator",
"history", "screen", "performance", "console", "crypto", "localStorage", "sessionStorage",
"indexedDB", "caches", "clientInformation", "external", "chrome", "browser", "JSON", "Math",
"Intl", "Reflect", "Atomics", "Temporal", "WebAssembly", "CSS", "Infinity", "NaN", "undefined",
]);
const browserNumbers = new Set(["innerWidth", "innerHeight", "outerWidth", "outerHeight", "screenX", "screenY", "screenLeft", "screenTop", "scrollX", "scrollY", "pageXOffset", "pageYOffset", "devicePixelRatio", "length"]);
const unsafe = (key) => key === "__proto__" || key === "prototype" || key === "constructor" ||
key.startsWith("__hackEngine") || key.startsWith("__ruffleMemoryInspector");
const descriptor = (object, key) => {
try { const d = Object.getOwnPropertyDescriptor(object, key); return d && "value" in d ? d : null; }
catch { return null; }
};
const traversable = (object) => {
if (!object || typeof object !== "object" || object === root) return false;
try {
if (ArrayBuffer.isView(object)) return Boolean(numericArrays[typedArrayTag.call(object)]) && typeof typedArrayLength.call(object) === "number";
const p = Object.getPrototypeOf(object);
return Array.isArray(object) || p === null || p === Object.prototype;
} catch { return false; }
};
const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
const typedArrayTag = Object.getOwnPropertyDescriptor(typedArrayPrototype, Symbol.toStringTag).get;
const typedArrayLength = Object.getOwnPropertyDescriptor(typedArrayPrototype, "length").get;
const numericArrays = { Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array };
const storageTypes = { Int8Array: "i8", Uint8Array: "u8", Uint8ClampedArray: "u8", Int16Array: "i16", Uint16Array: "u16", Int32Array: "i32", Uint32Array: "u32", Float32Array: "f32", Float64Array: "f64" };
const supportedTypes = ["number", ...new Set(Object.values(storageTypes))];
const storageTypeFor = (owner, key) => {
if (ArrayBuffer.isView(owner) && /^(0|[1-9][0-9]*)$/.test(key) && Number(key) < typedArrayLength.call(owner)) {
return storageTypes[typedArrayTag.call(owner)] || "number";
}
return "number";
};
const initial = new Map();
for (const key of Object.getOwnPropertyNames(root)) {
const d = descriptor(root, key);
if (d && excluded.has(key)) initial.set(key, d.value);
}
globalThis.__hackEngineJavaScript = ({ documentId, yieldToPage = () => new Promise((resolve) => setTimeout(resolve, 0)) }) => {
const id = `${documentId}.js`;
const handles = new Map();
const owners = new WeakMap();
let nextAddress = 1;
const MAX_HANDLES = 200000;
const displayPath = (path) => path.map((key, index) => /^[A-Za-z_$][\w$]*$/.test(key)
? `${index ? "." : ""}${key}` : `[${JSON.stringify(key)}]`).join("");
const eligibleRoot = (key, d) => !unsafe(key) && !browserNumbers.has(key) && d &&
(!excluded.has(key) || initial.has(key) && initial.get(key) !== d.value) &&
(traversable(d.value) || typeof d.value === "number" && Number.isFinite(d.value));
const roots = () => Object.getOwnPropertyNames(root).flatMap((key) => {
const d = descriptor(root, key);
return eligibleRoot(key, d) ? [{ path: [key], displayPath: displayPath([key]), kind: typeof d.value === "number" ? "number" : "object" }] : [];
});
const fail = (record) => { if (record) record.invalid = true; throw new Error("JavaScript target is stale; scan again to find the current object."); };
const validate = (record) => {
if (!record || record.invalid) return fail(record);
let current = root;
for (const step of record.chain) {
const d = descriptor(current, step.key);
if (!d || d.value !== step.object) return fail(record);
current = d.value;
}
if (current !== record.owner) return fail(record);
const d = descriptor(current, record.key);
if (!d || typeof d.value !== "number" || !Number.isFinite(d.value)) return fail(record);
return d;
};
const metadata = (address) => {
const record = handles.get(address);
const d = validate(record);
return { address, type: "number", storageType: storageTypeFor(record.owner, record.key), kind: "javascript", path: [...record.path], displayPath: displayPath(record.path), value: d.value, writable: Boolean(d.writable) };
};
const register = (owner, key, chain, path) => {
let byKey = owners.get(owner);
if (!byKey) owners.set(owner, byKey = new Map());
let record = byKey.get(key);
if (record) { try { validate(record); } catch { record = null; } }
if (!record) {
if (handles.size >= MAX_HANDLES) { const error = new Error("JavaScript handle limit reached; reload the game to release retained targets."); error.code = "HANDLE_LIMIT"; throw error; }
record = { address: nextAddress++, owner, key, chain, path: [...path], invalid: false };
byKey.set(key, record); handles.set(record.address, record);
}
return metadata(record.address);
};
const resolve = (path) => {
if (!Array.isArray(path) || !path.length || path.length > 9 || path.some((key) => typeof key !== "string" || unsafe(key))) throw new Error("Invalid JavaScript property path.");
let owner = root;
const chain = [];
for (let i = 0; i < path.length; i++) {
const key = path[i]; const d = descriptor(owner, key);
if (!d || i === 0 && !eligibleRoot(key, d)) throw new Error("JavaScript property path is unavailable.");
if (i === path.length - 1) {
if (typeof d.value !== "number" || !Number.isFinite(d.value)) throw new Error("JavaScript property is not a finite number.");
return register(owner, key, chain, path);
}
if (!traversable(d.value)) throw new Error("JavaScript property path is not inspectable.");
chain.push({ key, object: d.value }); owner = d.value;
}
};
const discover = async ({ requestId, rootPath, type = "smart" } = {}) => {
if (!["smart", "auto", ...supportedTypes].includes(type)) throw new Error("Unsupported JavaScript scan type.");
const matchesType = (owner, key) => type === "smart" || type === "auto" || storageTypeFor(owner, key) === type;
const coverage = { complete: true, objects: 0, properties: 0, numbers: 0, limits: { depth: 8, objects: 20000, properties: 100000, handles: MAX_HANDLES }, reasons: [] };
const incomplete = (reason) => { coverage.complete = false; if (!coverage.reasons.includes(reason)) coverage.reasons.push(reason); };
// Retire stale owners between scans; live handles (including watches) are never evicted.
let checked = 0;
for (const [address, record] of handles) {
try { validate(record); } catch { handles.delete(address); const byKey = owners.get(record.owner); if (byKey?.get(record.key) === record) byKey.delete(record.key); }
if (++checked % 500 === 0) await yieldToPage(requestId);
}
const entries = [];
const visited = new WeakSet();
const queue = [];
const selected = rootPath == null ? roots().map((r) => r.path) : [rootPath];
for (const path of selected) {
if (coverage.properties >= coverage.limits.properties) { incomplete("property limit"); break; }
coverage.properties++;
if (coverage.properties % 500 === 0) await yieldToPage(requestId);
if (!Array.isArray(path) || !path.length || path.length > 9 || path.some((key) => typeof key !== "string" || unsafe(key))) throw new Error("Invalid JavaScript root path.");
let owner = root; const chain = [];
for (let i = 0; i < path.length; i++) {
const key = path[i]; const d = descriptor(owner, key);
if (!d || i === 0 && !eligibleRoot(key, d)) throw new Error("JavaScript root is unavailable.");
if (i === path.length - 1 && typeof d.value === "number" && Number.isFinite(d.value)) {
try { if (matchesType(owner, key)) entries.push(register(owner, key, chain, path)); }
catch (error) { incomplete(error.code === "HANDLE_LIMIT" ? "handle limit" : "object changed during scan"); }
break;
}
if (!traversable(d.value)) throw new Error("JavaScript root is not inspectable.");
chain.push({ key, object: d.value }); owner = d.value;
if (i === path.length - 1) {
if (path.length >= 9) incomplete("depth limit");
else if (queue.length >= coverage.limits.objects) incomplete("object limit");
else queue.push({ object: owner, chain, path, depth: path.length - 1 });
}
}
}
await yieldToPage(requestId);
let work = 0;
for (let index = 0; index < queue.length; index++) {
const item = queue[index];
if (visited.has(item.object)) continue;
if (coverage.objects >= coverage.limits.objects) { incomplete("object limit"); break; }
visited.add(item.object); coverage.objects++;
let keys;
try {
// Avoid allocating a key list proportional to a very large typed array.
if (ArrayBuffer.isView(item.object)) {
const length = typedArrayLength.call(item.object);
keys = (function* () { for (let i = 0; i < length; i++) yield String(i); })();
} else keys = Object.getOwnPropertyNames(item.object);
} catch { incomplete("unreadable object"); continue; }
for (const key of keys) {
if (coverage.properties >= coverage.limits.properties) { incomplete("property limit"); break; }
coverage.properties++; work++;
if (work % 500 === 0) await yieldToPage(requestId);
if (unsafe(key)) continue;
const d = descriptor(item.object, key);
if (!d) continue;
const path = [...item.path, key];
if (typeof d.value === "number" && Number.isFinite(d.value)) {
try { if (matchesType(item.object, key)) entries.push(register(item.object, key, item.chain, path)); } catch (error) { incomplete(error.code === "HANDLE_LIMIT" ? "handle limit" : "object changed during scan"); }
} else if (traversable(d.value) && !visited.has(d.value)) {
if (item.depth >= coverage.limits.depth - 1) incomplete("depth limit");
else if (queue.length >= coverage.limits.objects) incomplete("object limit");
else queue.push({ object: d.value, chain: [...item.chain, { key, object: d.value }], path, depth: item.depth + 1 });
}
}
if (coverage.properties >= coverage.limits.properties) {
if (index + 1 < queue.length) incomplete("property limit");
break;
}
}
coverage.numbers = entries.length;
return { entries, coverage };
};
return {
describe: () => ({ id, kind: "javascript", displayName: "JavaScript objects", memoryBytes: 0, documentId, operations: ["scan", "refine", "watch", "write", "freeze"], capabilities: ["scan", "watch", "write", "freeze", "undo", "restore"], supportedTypes: [...supportedTypes] }),
roots, discover, resolve, metadata,
read: (address) => validate(handles.get(address)).value,
write(address, value) {
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("Enter a finite JavaScript number.");
const record = handles.get(address); const d = validate(record);
if (!d.writable) throw new Error("JavaScript property is read-only.");
if (ArrayBuffer.isView(record.owner) && /^(0|[1-9][0-9]*)$/.test(record.key)) {
const Type = numericArrays[typedArrayTag.call(record.owner)];
if (!Type || !Object.is(new Type([value])[0], value)) throw new Error("JavaScript write verification failed: value cannot be represented by this typed array.");
}
if (!Reflect.defineProperty(record.owner, record.key, { ...d, value })) throw new Error("JavaScript property could not be written.");
const actual = validate(record).value;
if (!Object.is(actual, value)) throw new Error("JavaScript write verification failed.");
return actual;
},
};
};
})();