@coana-tech/cli 15.0.1 → 15.0.2
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/cli.mjs +593 -474
- package/package.json +1 -1
- package/reachability-analyzers-cli.mjs +14840 -13722
- package/repos/coana-tech/goana/bin/goana-darwin-amd64.gz +0 -0
- package/repos/coana-tech/goana/bin/goana-darwin-arm64.gz +0 -0
- package/repos/coana-tech/goana/bin/goana-linux-amd64.gz +0 -0
- package/repos/coana-tech/goana/bin/goana-linux-arm64.gz +0 -0
- package/repos/coana-tech/javap-service/javap-service.jar +0 -0
- package/repos/coana-tech/jelly-private/dist/bundle/jelly.js +25 -6
- package/repos/coana-tech/spar/runtime/dist/approx.js +1060 -0
- package/repos/coana-tech/spar/runtime/dist/hints.js +131 -0
- package/repos/coana-tech/spar/runtime/dist/hooks.js +172 -0
- package/repos/coana-tech/spar/runtime/dist/logger.js +66 -0
- package/repos/coana-tech/spar/runtime/dist/options.js +10 -0
- package/repos/coana-tech/spar/runtime/dist/proxy.js +119 -0
- package/repos/coana-tech/spar/runtime/dist/sandbox.js +84 -0
- package/repos/coana-tech/spar/runtime/dist/types.js +3 -0
- package/repos/coana-tech/spar/sparjs-aarch64-apple-darwin.gz +0 -0
- package/repos/coana-tech/spar/sparjs-aarch64-unknown-linux-musl.gz +0 -0
- package/repos/coana-tech/spar/sparjs-x86_64-apple-darwin.gz +0 -0
- package/repos/coana-tech/spar/sparjs-x86_64-unknown-linux-musl.gz +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Hint collection for approximate interpretation.
|
|
2
|
+
// Adapted from Jelly's src/approx/hints.ts.
|
|
3
|
+
// Save native functions before user code can overwrite them.
|
|
4
|
+
const arrayFrom = Array.from.bind(Array);
|
|
5
|
+
const arrayFlat = Array.prototype.flat;
|
|
6
|
+
function flattenValues(map) {
|
|
7
|
+
return arrayFlat.call(arrayFrom(map.values()));
|
|
8
|
+
}
|
|
9
|
+
function mapArrayAddNoDuplicates(key, value, map, eq) {
|
|
10
|
+
let arr = map.get(key);
|
|
11
|
+
if (!arr) {
|
|
12
|
+
arr = [];
|
|
13
|
+
map.set(key, arr);
|
|
14
|
+
}
|
|
15
|
+
if (!arr.some(v => eq(v, value)))
|
|
16
|
+
arr.push(value);
|
|
17
|
+
}
|
|
18
|
+
function mapArraySize(map) {
|
|
19
|
+
let c = 0;
|
|
20
|
+
for (const v of map.values())
|
|
21
|
+
c += v.length;
|
|
22
|
+
return c;
|
|
23
|
+
}
|
|
24
|
+
export class Hints {
|
|
25
|
+
/**
|
|
26
|
+
* Names of modules (including eval code) that have been executed dynamically.
|
|
27
|
+
*/
|
|
28
|
+
modules = [];
|
|
29
|
+
/**
|
|
30
|
+
* Map from module name to index in LocationJSON strings.
|
|
31
|
+
*/
|
|
32
|
+
moduleIndex = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Functions that have been visited (either via module execution or by forced execution).
|
|
35
|
+
*/
|
|
36
|
+
functions = new Set();
|
|
37
|
+
/**
|
|
38
|
+
* Read hints grouped by location of the read operation.
|
|
39
|
+
*/
|
|
40
|
+
reads = new Map();
|
|
41
|
+
/**
|
|
42
|
+
* Write hints grouped by location of the write operation.
|
|
43
|
+
*/
|
|
44
|
+
writes = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Require/import hints grouped by location of the require/import operation.
|
|
47
|
+
*/
|
|
48
|
+
requires = new Map();
|
|
49
|
+
/**
|
|
50
|
+
* Eval/Function hints grouped by the location of the eval/Function operation.
|
|
51
|
+
*/
|
|
52
|
+
evals = new Map();
|
|
53
|
+
addModule(m) {
|
|
54
|
+
let idx = this.moduleIndex.get(m);
|
|
55
|
+
if (idx !== undefined)
|
|
56
|
+
return idx;
|
|
57
|
+
idx = this.modules.length;
|
|
58
|
+
this.modules.push(m);
|
|
59
|
+
this.moduleIndex.set(m, idx);
|
|
60
|
+
return idx;
|
|
61
|
+
}
|
|
62
|
+
addFunction(f) {
|
|
63
|
+
this.functions.add(f);
|
|
64
|
+
}
|
|
65
|
+
addReadHint(h) {
|
|
66
|
+
mapArrayAddNoDuplicates(h.loc, h, this.reads, (v1, v2) => v1.prop === v2.prop && v1.valLoc === v2.valLoc && v1.valType === v2.valType);
|
|
67
|
+
}
|
|
68
|
+
addWriteHint(h) {
|
|
69
|
+
mapArrayAddNoDuplicates(h.loc, h, this.writes, (v1, v2) => v1.type === v2.type && v1.baseLoc === v2.baseLoc && v1.baseType === v2.baseType &&
|
|
70
|
+
v1.prop === v2.prop && v1.valLoc === v2.valLoc && v1.valType === v2.valType);
|
|
71
|
+
}
|
|
72
|
+
addRequireHint(h) {
|
|
73
|
+
mapArrayAddNoDuplicates(h.loc, h, this.requires, (v1, v2) => v1.str === v2.str);
|
|
74
|
+
}
|
|
75
|
+
addEvalHint(h) {
|
|
76
|
+
mapArrayAddNoDuplicates(h.loc, h, this.evals, (v1, v2) => v1.str === v2.str);
|
|
77
|
+
}
|
|
78
|
+
add(newHints) {
|
|
79
|
+
const moduleReindex = new Map();
|
|
80
|
+
for (const [i, s] of newHints.modules.entries())
|
|
81
|
+
moduleReindex.set(i, this.addModule(s));
|
|
82
|
+
const convert = (loc) => `${moduleReindex.get(parseInt(loc))}${loc.substring(loc.indexOf(":"))}`;
|
|
83
|
+
for (const f of newHints.functions)
|
|
84
|
+
this.addFunction(convert(f));
|
|
85
|
+
for (const { loc, prop, valLoc, valType } of newHints.reads) {
|
|
86
|
+
this.addReadHint({
|
|
87
|
+
loc: convert(loc),
|
|
88
|
+
prop,
|
|
89
|
+
valLoc: convert(valLoc),
|
|
90
|
+
valType
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
for (const { type, loc, baseLoc, baseType, prop, valLoc, valType } of newHints.writes) {
|
|
94
|
+
this.addWriteHint({
|
|
95
|
+
type,
|
|
96
|
+
loc: convert(loc),
|
|
97
|
+
baseLoc: convert(baseLoc),
|
|
98
|
+
baseType,
|
|
99
|
+
prop,
|
|
100
|
+
valLoc: convert(valLoc),
|
|
101
|
+
valType
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const { loc, str } of newHints.requires)
|
|
105
|
+
this.addRequireHint({
|
|
106
|
+
loc: convert(loc),
|
|
107
|
+
str
|
|
108
|
+
});
|
|
109
|
+
for (const { loc, str } of newHints.evals)
|
|
110
|
+
this.addEvalHint({
|
|
111
|
+
loc: convert(loc),
|
|
112
|
+
str
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
toJSON() {
|
|
116
|
+
return {
|
|
117
|
+
modules: this.modules,
|
|
118
|
+
functions: arrayFrom(this.functions),
|
|
119
|
+
reads: flattenValues(this.reads),
|
|
120
|
+
writes: flattenValues(this.writes),
|
|
121
|
+
requires: flattenValues(this.requires),
|
|
122
|
+
evals: flattenValues(this.evals)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
clearHints() {
|
|
126
|
+
this.reads.clear();
|
|
127
|
+
this.writes.clear();
|
|
128
|
+
this.requires.clear();
|
|
129
|
+
this.evals.clear();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// ESM module loader hooks for approximate interpretation.
|
|
2
|
+
// Adapted from Jelly's src/approx/hooks.ts.
|
|
3
|
+
// Code transformation is requested directly from the Rust parent process,
|
|
4
|
+
// bypassing the main thread to avoid deadlocks.
|
|
5
|
+
//
|
|
6
|
+
// IPC: writes transform requests to the IPC FIFO (shared with main thread),
|
|
7
|
+
// reads responses from the hooks FIFO (dedicated channel from Rust parent).
|
|
8
|
+
import Module, { createRequire } from "module";
|
|
9
|
+
import { extname } from "path";
|
|
10
|
+
import { WHITELISTED } from "./sandbox.js";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { openSync, readFileSync, readSync, realpathSync, writeSync } from "fs";
|
|
13
|
+
const PREFIX = "_J$";
|
|
14
|
+
let port2;
|
|
15
|
+
/** Basedir for filtering modules. */
|
|
16
|
+
let resolvedBasedir = "";
|
|
17
|
+
/** File descriptor for writing to the IPC FIFO (Node.js → Rust). */
|
|
18
|
+
let ipcFd;
|
|
19
|
+
/** File descriptor for reading from the hooks FIFO (Rust → hooks). */
|
|
20
|
+
let hooksFd;
|
|
21
|
+
const responseBuffer = Buffer.alloc(65536);
|
|
22
|
+
let responseLeftover = "";
|
|
23
|
+
/**
|
|
24
|
+
* Synchronous line read from the hooks response FIFO.
|
|
25
|
+
*/
|
|
26
|
+
function readLineSync() {
|
|
27
|
+
while (true) {
|
|
28
|
+
const nlIdx = responseLeftover.indexOf('\n');
|
|
29
|
+
if (nlIdx !== -1) {
|
|
30
|
+
const line = responseLeftover.substring(0, nlIdx);
|
|
31
|
+
responseLeftover = responseLeftover.substring(nlIdx + 1);
|
|
32
|
+
return line;
|
|
33
|
+
}
|
|
34
|
+
const bytesRead = readSync(hooksFd, responseBuffer);
|
|
35
|
+
if (bytesRead === 0)
|
|
36
|
+
throw new Error("hooks response FIFO closed unexpectedly");
|
|
37
|
+
responseLeftover += responseBuffer.toString("utf8", 0, bytesRead);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sends a JSON message to the Rust parent via the IPC FIFO.
|
|
42
|
+
*/
|
|
43
|
+
function sendToParent(msg) {
|
|
44
|
+
const json = JSON.stringify(msg) + '\n';
|
|
45
|
+
writeSync(ipcFd, json);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Requests code transformation from the Rust parent process.
|
|
49
|
+
* Synchronous: sends request on IPC FIFO, reads response from hooks FIFO.
|
|
50
|
+
*/
|
|
51
|
+
function requestTransform(file, source) {
|
|
52
|
+
sendToParent({ transform: file, source, sourceType: "module", hooks: true });
|
|
53
|
+
const line = readLineSync();
|
|
54
|
+
const resp = JSON.parse(line);
|
|
55
|
+
return resp.transformed;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Instruments an ESM module by requesting transformation from the Rust parent.
|
|
59
|
+
*/
|
|
60
|
+
function transformModule(filename, code) {
|
|
61
|
+
let resolvedFilename = filename;
|
|
62
|
+
try {
|
|
63
|
+
resolvedFilename = realpathSync(filename);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
if (!resolvedFilename.startsWith(resolvedBasedir)) {
|
|
67
|
+
log("verbose", `Ignoring module outside basedir: ${filename}`);
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
log("info", `Loading module ${filename} (${Math.ceil(code.length / 1024)}KB)`);
|
|
71
|
+
log("verbose", `Instrumenting ${filename}`);
|
|
72
|
+
const transformed = requestTransform(filename, code);
|
|
73
|
+
// Send code size metadata back to the main thread
|
|
74
|
+
port2.postMessage({ type: "metadata", codeSize: code.length });
|
|
75
|
+
return transformed;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Module hooks initialization.
|
|
79
|
+
* (Registered in approx.ts.)
|
|
80
|
+
*/
|
|
81
|
+
export async function initialize({ opts, port2: p2, ipcFifoPath, hooksFifoPath }) {
|
|
82
|
+
port2 = p2;
|
|
83
|
+
const basedir = opts.basedir;
|
|
84
|
+
try {
|
|
85
|
+
resolvedBasedir = realpathSync(basedir);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
resolvedBasedir = basedir;
|
|
89
|
+
}
|
|
90
|
+
ipcFd = openSync(ipcFifoPath, "w");
|
|
91
|
+
hooksFd = openSync(hooksFifoPath, "r");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Module resolve hook.
|
|
95
|
+
*/
|
|
96
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
97
|
+
if (context.parentURL === `file://${import.meta.dirname}/approx.js`)
|
|
98
|
+
context.parentURL = undefined; // approx.js is entry
|
|
99
|
+
log("verbose", `Resolving ${specifier}${context.parentURL ? ` from ${context.parentURL}` : " (entry)"}`);
|
|
100
|
+
const str = specifier.startsWith("node:") ? specifier.substring(5) : specifier;
|
|
101
|
+
if (Module.isBuiltin(str) && !WHITELISTED.has(str))
|
|
102
|
+
return {
|
|
103
|
+
format: "commonjs",
|
|
104
|
+
url: `node:${str}`, // signals to load hook that this is a sandboxed builtin
|
|
105
|
+
shortCircuit: true
|
|
106
|
+
};
|
|
107
|
+
// Simplified: no TypeScript module resolution (unlike Jelly)
|
|
108
|
+
return nextResolve(str);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Module loading hook.
|
|
112
|
+
*/
|
|
113
|
+
export async function load(url, context, nextLoad) {
|
|
114
|
+
try {
|
|
115
|
+
const ext = extname(url);
|
|
116
|
+
if ([".ts", ".tsx", ".mts", ".cts", ".jsx"].includes(ext))
|
|
117
|
+
context.format = "module";
|
|
118
|
+
else if (ext === "" && url.startsWith("file://")) {
|
|
119
|
+
try {
|
|
120
|
+
const content = readFileSync(fileURLToPath(url), "utf8");
|
|
121
|
+
if (content.startsWith("#!"))
|
|
122
|
+
context.format = "commonjs";
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
}
|
|
126
|
+
const sandboxedBuiltin = url.startsWith("node:");
|
|
127
|
+
log("verbose", `Loading ${url} (ESM loader, format: ${sandboxedBuiltin ? "sandboxed builtin" : context.format})`);
|
|
128
|
+
const res = await nextLoad(url, context); // TODO: fails with ERR_UNSUPPORTED_ESM_URL_SCHEME on https and http
|
|
129
|
+
if (sandboxedBuiltin) {
|
|
130
|
+
const m = url.substring(5);
|
|
131
|
+
log("verbose", `Intercepting import "${m}"`);
|
|
132
|
+
res.source = `const m = globalThis.${PREFIX}builtin["${m}"];`;
|
|
133
|
+
for (const p of Object.getOwnPropertyNames(require(m)))
|
|
134
|
+
res.source += `module.exports["${p}"] = m["${p}"];`;
|
|
135
|
+
return res;
|
|
136
|
+
}
|
|
137
|
+
if (res.format === "builtin" || res.format === "commonjs" || res.format === "json")
|
|
138
|
+
return res;
|
|
139
|
+
if (res.format !== "module")
|
|
140
|
+
log("warn", `Ignoring ${url} (format: ${res.format})`);
|
|
141
|
+
else if (!url.startsWith("file://"))
|
|
142
|
+
log("error", `Error: Unsupported URL scheme ${url}`);
|
|
143
|
+
else if (!(res.source instanceof Uint8Array))
|
|
144
|
+
log("error", `Error: Unexpected source type for ${url}`);
|
|
145
|
+
else {
|
|
146
|
+
const filename = fileURLToPath(url);
|
|
147
|
+
const source = new TextDecoder().decode(res.source);
|
|
148
|
+
const transformed = transformModule(filename, source);
|
|
149
|
+
return {
|
|
150
|
+
format: "module",
|
|
151
|
+
shortCircuit: true,
|
|
152
|
+
source: transformed
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
log("error", `Suppressed exception at module load: ${err instanceof Error ? err.stack : err}`);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
format: "commonjs",
|
|
161
|
+
shortCircuit: true,
|
|
162
|
+
source: `module.exports = ${PREFIX}proxy`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* TTY doesn't work for hooks thread, so logging is done via the approximate interpretation thread.
|
|
167
|
+
* Note: the logging from this thread is asynchronous so the messages may be written delayed to the log!
|
|
168
|
+
*/
|
|
169
|
+
function log(level, str) {
|
|
170
|
+
port2.postMessage({ type: "log", level, str });
|
|
171
|
+
}
|
|
172
|
+
const require = createRequire(import.meta.url);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Logger that writes to stdout (matching Jelly).
|
|
2
|
+
// IPC uses dedicated FIFOs, so stdout is free for logging.
|
|
3
|
+
import { openSync, truncateSync, writeSync } from "fs";
|
|
4
|
+
const LEVELS = {
|
|
5
|
+
error: 0,
|
|
6
|
+
warn: 1,
|
|
7
|
+
info: 2,
|
|
8
|
+
verbose: 3,
|
|
9
|
+
debug: 4,
|
|
10
|
+
};
|
|
11
|
+
const COLORS = {
|
|
12
|
+
error: "\x1b[31m", // red
|
|
13
|
+
warn: "\x1b[33m", // yellow
|
|
14
|
+
verbose: "\x1b[32m", // green
|
|
15
|
+
debug: "\x1b[36m", // cyan
|
|
16
|
+
};
|
|
17
|
+
const RESET = "\x1b[0m";
|
|
18
|
+
const WHITE = "\x1b[97m";
|
|
19
|
+
const BOLD = "\x1b[1m";
|
|
20
|
+
const CLEAR = "\x1b[0K";
|
|
21
|
+
// Save stdout before sandboxing patches it.
|
|
22
|
+
const stdout = process.stdout;
|
|
23
|
+
let useColors = stdout.isTTY ?? false;
|
|
24
|
+
let currentLevel = LEVELS.info;
|
|
25
|
+
let logFd;
|
|
26
|
+
function shouldLog(level) {
|
|
27
|
+
return (LEVELS[level] ?? 0) <= currentLevel;
|
|
28
|
+
}
|
|
29
|
+
function log(level, msg) {
|
|
30
|
+
if (shouldLog(level)) {
|
|
31
|
+
if (logFd !== undefined) {
|
|
32
|
+
writeSync(logFd, `${msg}\n`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const color = useColors ? (COLORS[level] ?? "") : "";
|
|
36
|
+
const reset = color ? RESET : "";
|
|
37
|
+
stdout.write(`${color}${msg}${reset}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const logger = {
|
|
42
|
+
get level() { return Object.entries(LEVELS).find(([_, v]) => v === currentLevel)?.[0] ?? "info"; },
|
|
43
|
+
set level(l) { currentLevel = LEVELS[l] ?? LEVELS.info; },
|
|
44
|
+
error: (msg) => log("error", msg),
|
|
45
|
+
warn: (msg) => log("warn", msg),
|
|
46
|
+
warning: (msg) => log("warn", msg),
|
|
47
|
+
info: (msg) => log("info", msg),
|
|
48
|
+
verbose: (msg) => log("verbose", msg),
|
|
49
|
+
debug: (msg) => log("debug", msg),
|
|
50
|
+
isInfoEnabled: () => shouldLog("info"),
|
|
51
|
+
isVerboseEnabled: () => shouldLog("verbose"),
|
|
52
|
+
isDebugEnabled: () => shouldLog("debug"),
|
|
53
|
+
};
|
|
54
|
+
export default logger;
|
|
55
|
+
export function logToFile(path) {
|
|
56
|
+
truncateSync(path, 0);
|
|
57
|
+
logFd = openSync(path, "w");
|
|
58
|
+
useColors = false;
|
|
59
|
+
}
|
|
60
|
+
function writeStdOut(s) {
|
|
61
|
+
stdout.write(WHITE + BOLD + s.substring(0, stdout.columns) + RESET + CLEAR + "\r");
|
|
62
|
+
}
|
|
63
|
+
export function writeStdOutIfActive(s) {
|
|
64
|
+
if (!logFd && useColors && currentLevel === LEVELS.info)
|
|
65
|
+
writeStdOut(s);
|
|
66
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Proxy objects for untracked values during approximate interpretation.
|
|
2
|
+
// Copied from Jelly's src/approx/proxy.ts.
|
|
3
|
+
const handler = {
|
|
4
|
+
get(target, p, receiver) {
|
|
5
|
+
switch (p) {
|
|
6
|
+
case "length":
|
|
7
|
+
if (receiver === theArgumentsProxy)
|
|
8
|
+
return 10; // number of mock arguments provided at forced execution of functions
|
|
9
|
+
else
|
|
10
|
+
return 1; // value for other array lengths
|
|
11
|
+
case Symbol.toPrimitive:
|
|
12
|
+
return function (hint) {
|
|
13
|
+
switch (hint) {
|
|
14
|
+
case "number":
|
|
15
|
+
return 0;
|
|
16
|
+
case "string":
|
|
17
|
+
return "";
|
|
18
|
+
case "default":
|
|
19
|
+
return "0"; // TODO: appropriate value?
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
case Symbol.iterator:
|
|
23
|
+
return function () {
|
|
24
|
+
let index = 0;
|
|
25
|
+
return {
|
|
26
|
+
next: () => {
|
|
27
|
+
if (index++ < 3)
|
|
28
|
+
return { value: theProxy, done: false };
|
|
29
|
+
else
|
|
30
|
+
return { value: undefined, done: true };
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
// TODO: other standard symbols that should be modeled? see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
|
|
35
|
+
default:
|
|
36
|
+
const desc = Object.getOwnPropertyDescriptor(target, p);
|
|
37
|
+
if (desc && !desc.configurable && !desc.writable) // non-writable, non-configurable properties must return target value
|
|
38
|
+
return target[p];
|
|
39
|
+
return theProxy;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
set() {
|
|
43
|
+
return false; // if returning true, some native functions get stuck in proxy loop
|
|
44
|
+
},
|
|
45
|
+
has() {
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
apply() {
|
|
49
|
+
return theProxy;
|
|
50
|
+
},
|
|
51
|
+
construct() {
|
|
52
|
+
return theProxy;
|
|
53
|
+
},
|
|
54
|
+
defineProperty() {
|
|
55
|
+
return false;
|
|
56
|
+
},
|
|
57
|
+
deleteProperty() {
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
getOwnPropertyDescriptor(target, property) {
|
|
61
|
+
if (property === "prototype")
|
|
62
|
+
return Object.getOwnPropertyDescriptor(target, property);
|
|
63
|
+
return {
|
|
64
|
+
configurable: true,
|
|
65
|
+
enumerable: true,
|
|
66
|
+
value: theProxy,
|
|
67
|
+
writable: true
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
isExtensible() {
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
setPrototypeOf() {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export const theProxy = new Proxy(function theProxy() { }, handler);
|
|
78
|
+
export const theArgumentsProxy = new Proxy([], handler);
|
|
79
|
+
export function makeBaseProxy(target) {
|
|
80
|
+
if (!target || !(typeof target === "object" || typeof target === "function"))
|
|
81
|
+
return theProxy;
|
|
82
|
+
return new Proxy(target, {
|
|
83
|
+
get(target, prop, receiver) {
|
|
84
|
+
if (prop in target)
|
|
85
|
+
return Reflect.get(target, prop, receiver);
|
|
86
|
+
else {
|
|
87
|
+
const desc = Object.getOwnPropertyDescriptor(target, prop);
|
|
88
|
+
if (desc && !desc.configurable && !desc.writable) // non-writable, non-configurable properties must return target value
|
|
89
|
+
return target[prop];
|
|
90
|
+
return theProxy;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
has(_target, _prop) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export function isProxy(x) {
|
|
99
|
+
return x === theProxy || x === theArgumentsProxy;
|
|
100
|
+
}
|
|
101
|
+
export function stdlibProxy(obj) {
|
|
102
|
+
return new Proxy(obj, {
|
|
103
|
+
get: function (target, prop) {
|
|
104
|
+
const desc = Object.getOwnPropertyDescriptor(target, prop);
|
|
105
|
+
if (desc && !desc.configurable && !desc.writable) // non-writable, non-configurable properties must return target value
|
|
106
|
+
return target[prop];
|
|
107
|
+
if (typeof desc?.value === "function")
|
|
108
|
+
return theProxy;
|
|
109
|
+
return desc?.value;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export function makeModuleProxy(target) {
|
|
114
|
+
return new Proxy(target, {
|
|
115
|
+
get(target, prop, _receiver) {
|
|
116
|
+
return prop === "constructor" ? theProxy : target[prop];
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Sandboxes global builtins for approximate interpretation.
|
|
2
|
+
// Adapted from Jelly's src/approx/sandbox.ts.
|
|
3
|
+
import Module from "module";
|
|
4
|
+
import { theProxy } from "./proxy.js";
|
|
5
|
+
/** Prefix for special global variables. */
|
|
6
|
+
const PREFIX = "_J$";
|
|
7
|
+
/**
|
|
8
|
+
* Names of the special _J$* globals that should be frozen. */
|
|
9
|
+
const SPECIALS = new Set(["start", "pw", "dpr", "alloc", "init", "method", "comp", "new", "enter", "catch", "loop", "eval", "cr", "freeze", "fun", "require", "this"]
|
|
10
|
+
.map(s => PREFIX + s));
|
|
11
|
+
/**
|
|
12
|
+
* Standard library modules that are not mocked.
|
|
13
|
+
*/
|
|
14
|
+
export const WHITELISTED = new Set([
|
|
15
|
+
"events", "buffer", "assert", "assert/strict", "constants", "crypto",
|
|
16
|
+
"string_decoder", "util", "util/types", "path", "url", "tty", "sys"
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Sandboxes global builtins.
|
|
20
|
+
*/
|
|
21
|
+
export function patchGlobalBuiltins() {
|
|
22
|
+
const emptyFunction = function () { };
|
|
23
|
+
const invokeCallbackWithArgs = function (cb, ...args) { cb(...args); };
|
|
24
|
+
const invokeCallbackWithArgs2 = function (cb, _c, ...args) { cb(...args); };
|
|
25
|
+
// replace globals
|
|
26
|
+
const g = globalThis;
|
|
27
|
+
g.clearImmediate = g.clearInterval = g.clearTimeout = g.fetch = emptyFunction;
|
|
28
|
+
g.setImmediate = g.queueMicrotask = invokeCallbackWithArgs;
|
|
29
|
+
g.setInterval = g.setTimeout = invokeCallbackWithArgs2;
|
|
30
|
+
// replace process.*
|
|
31
|
+
const p = process;
|
|
32
|
+
p.on = p.send = p.chdir = p.exit = p.reallyExit = p.abort = p.dlopen = p.kill = p.openStdin = p.binding = p._linkedBinding = p.removeAllListeners = p.removeListener = p.off = theProxy;
|
|
33
|
+
p.nextTick = invokeCallbackWithArgs;
|
|
34
|
+
for (const prop of ["stdin", "stdout", "stderr"])
|
|
35
|
+
Object.defineProperty(p, prop, { value: theProxy });
|
|
36
|
+
// replace console.*
|
|
37
|
+
const c = console;
|
|
38
|
+
for (const p of Object.getOwnPropertyNames(Object.getPrototypeOf(console)))
|
|
39
|
+
if (typeof c[p] === "function")
|
|
40
|
+
c[p] = emptyFunction;
|
|
41
|
+
c._stdout = c._stderr = theProxy;
|
|
42
|
+
// replace Error.*, Atomics.*, Module.*
|
|
43
|
+
Error.captureStackTrace = emptyFunction; // TODO: Error (parser writes to Error.prepareStackTrace)
|
|
44
|
+
Atomics.wait = Atomics.waitAsync = theProxy;
|
|
45
|
+
Module.register = function () { };
|
|
46
|
+
// replace Module._load
|
|
47
|
+
const realLoad = Module._load;
|
|
48
|
+
Module._load = function (request, parent, isMain) {
|
|
49
|
+
const result = realLoad.call(this, request, parent, isMain);
|
|
50
|
+
const name = request.startsWith("node:") ? request.substring(5) : request;
|
|
51
|
+
if (Module.isBuiltin(name) && !WHITELISTED.has(name))
|
|
52
|
+
return globalThis[PREFIX + "builtin"]?.[name] ?? theProxy;
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
// define typical test framework functions
|
|
56
|
+
g.describe = g.it = g.before = g.beforeAll = g.beforeEach = g.after = g.afterAll = g.afterEach = g.test = g.define = emptyFunction;
|
|
57
|
+
g.describe.skip = emptyFunction;
|
|
58
|
+
g.expect = theProxy;
|
|
59
|
+
// TODO: assign theProxy to all undeclared variables?
|
|
60
|
+
g.Worker = theProxy;
|
|
61
|
+
// DOM specific interface
|
|
62
|
+
g.window = g.document = theProxy;
|
|
63
|
+
// freeze objects and properties
|
|
64
|
+
for (const x of [
|
|
65
|
+
Array, ArrayBuffer, BigInt, Boolean, DataView, Date, Error, AggregateError, EvalError, RangeError, ReferenceError,
|
|
66
|
+
SyntaxError, TypeError, URIError, Intl, Int8Array, Uint8Array, Uint16Array, Int16Array, Uint32Array, Int32Array,
|
|
67
|
+
Float32Array, Float64Array, Uint8ClampedArray, BigUint64Array, BigInt64Array, FinalizationRegistry, JSON, Map,
|
|
68
|
+
Math, Number, Object, Promise, Proxy, Reflect, RegExp, Set, String, Symbol, WeakMap, WeakRef, WeakSet
|
|
69
|
+
]) {
|
|
70
|
+
Object.freeze(x);
|
|
71
|
+
if ("prototype" in x)
|
|
72
|
+
Object.freeze(x.prototype);
|
|
73
|
+
}
|
|
74
|
+
Object.freeze(Function);
|
|
75
|
+
for (const p of ["apply", "bind", "call"]) // must allow overwriting toString at functions
|
|
76
|
+
Object.defineProperty(Function.prototype, p, { configurable: false, writable: false });
|
|
77
|
+
for (const p of ["globalThis", "global", "Infinity", "NaN", "undefined", "eval", "isFinite", "isNaN", "parseFloat", "parseInt", ...SPECIALS, "$log"])
|
|
78
|
+
Object.defineProperty(g, p, { configurable: false, writable: false });
|
|
79
|
+
Object.freeze(Module._extensions);
|
|
80
|
+
Object.freeze(Module);
|
|
81
|
+
Object.freeze(Module.prototype);
|
|
82
|
+
Object.freeze(process);
|
|
83
|
+
Object.freeze(console);
|
|
84
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|