368 lines
11 KiB
JavaScript
368 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
/*
|
|
* Classic-worker wrapper for the pinned WebPerl 0.09-beta Emscripten runtime.
|
|
* User-controlled values cross into Perl only through a JSON file in MEMFS.
|
|
* Successful replacement bytes return through a separate fixed binary file;
|
|
* the bounded JSON response contains metadata only.
|
|
* The sole call through WebPerl's eval export is the fixed source string
|
|
* "RegexTools::run_request()"; no request data is interpolated into code.
|
|
*/
|
|
|
|
var BRIDGE_PROTOCOL_VERSION = 1;
|
|
var REQUEST_PATH = "/tmp/regex-tools-request.json";
|
|
var RESPONSE_PATH = "/tmp/regex-tools-response.json";
|
|
var OUTPUT_PATH = "/tmp/regex-tools-output.bin";
|
|
var HARNESS_PATH = "/tmp/regex-tools-bridge.pl";
|
|
var FIXED_INVOCATION = "RegexTools::run_request()";
|
|
var MAXIMUM_PATTERN_CHARACTERS = 64 * 1024;
|
|
var MAXIMUM_SUBJECT_BYTES = 16 * 1024 * 1024;
|
|
var MAXIMUM_REPLACEMENT_CHARACTERS = 64 * 1024;
|
|
var MAXIMUM_MATCHES = 10000;
|
|
var MAXIMUM_CAPTURE_ROWS = 100000;
|
|
var MAXIMUM_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
var MAXIMUM_REQUEST_JSON_BYTES =
|
|
6 *
|
|
(MAXIMUM_SUBJECT_BYTES +
|
|
MAXIMUM_PATTERN_CHARACTERS +
|
|
MAXIMUM_REPLACEMENT_CHARACTERS) +
|
|
16 * 1024;
|
|
var MAXIMUM_RESPONSE_JSON_BYTES = 8 * 1024 * 1024;
|
|
var utf8Encoder = new TextEncoder();
|
|
var runtimeRoot = new URL("./", self.location.href);
|
|
var runtimeResolve;
|
|
var runtimeReject;
|
|
var runtimeReady = new Promise(function (resolve, reject) {
|
|
runtimeResolve = resolve;
|
|
runtimeReject = reject;
|
|
});
|
|
var stderrTail = "";
|
|
|
|
function boundedRuntimeError(value) {
|
|
var message = value instanceof Error ? value.message : String(value);
|
|
if (stderrTail) {
|
|
message += " (" + stderrTail.slice(-512) + ")";
|
|
}
|
|
return message.slice(0, 1024);
|
|
}
|
|
|
|
function captureStderr(value) {
|
|
stderrTail = (stderrTail + String(value) + "\n").slice(-4096);
|
|
}
|
|
|
|
function isRecord(value) {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function owns(value, key) {
|
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
}
|
|
|
|
function isIntegerInRange(value, minimum, maximum) {
|
|
return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
}
|
|
|
|
function hasExactKeys(value, expected) {
|
|
var actual = Object.keys(value).sort();
|
|
var wanted = expected.slice().sort();
|
|
if (actual.length !== wanted.length) return false;
|
|
for (var index = 0; index < actual.length; index += 1) {
|
|
if (actual[index] !== wanted[index]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function encodeRequest(request) {
|
|
if (
|
|
!isRecord(request) ||
|
|
request.abi !== 1 ||
|
|
typeof request.operation !== "string"
|
|
) {
|
|
throw new Error("The WebPerl bridge request is invalid.");
|
|
}
|
|
if (request.operation === "identity") {
|
|
if (!hasExactKeys(request, ["abi", "operation"])) {
|
|
throw new Error("The WebPerl identity request is invalid.");
|
|
}
|
|
} else {
|
|
var replacement = request.operation === "replace";
|
|
if (
|
|
(!replacement && request.operation !== "execute") ||
|
|
!hasExactKeys(
|
|
request,
|
|
[
|
|
"abi",
|
|
"operation",
|
|
"pattern",
|
|
"flags",
|
|
"options",
|
|
"subject",
|
|
"scanAll",
|
|
"maximumMatches",
|
|
"maximumCaptureRows",
|
|
].concat(replacement ? ["replacement", "maximumOutputBytes"] : []),
|
|
) ||
|
|
typeof request.pattern !== "string" ||
|
|
request.pattern.length > MAXIMUM_PATTERN_CHARACTERS ||
|
|
typeof request.flags !== "string" ||
|
|
request.flags.length > 10 ||
|
|
typeof request.subject !== "string" ||
|
|
typeof request.scanAll !== "boolean" ||
|
|
!isRecord(request.options) ||
|
|
Object.keys(request.options).length !== 0 ||
|
|
!isIntegerInRange(request.maximumMatches, 1, MAXIMUM_MATCHES) ||
|
|
!isIntegerInRange(request.maximumCaptureRows, 1, MAXIMUM_CAPTURE_ROWS)
|
|
) {
|
|
throw new Error("The WebPerl execution request is invalid.");
|
|
}
|
|
if (
|
|
utf8Encoder.encode(request.subject).byteLength > MAXIMUM_SUBJECT_BYTES
|
|
) {
|
|
throw new Error("The WebPerl subject exceeds its UTF-8 byte limit.");
|
|
}
|
|
if (
|
|
replacement &&
|
|
(typeof request.replacement !== "string" ||
|
|
request.replacement.length > MAXIMUM_REPLACEMENT_CHARACTERS ||
|
|
!isIntegerInRange(request.maximumOutputBytes, 1, MAXIMUM_OUTPUT_BYTES))
|
|
) {
|
|
throw new Error("The WebPerl replacement request is invalid.");
|
|
}
|
|
}
|
|
var encoded = utf8Encoder.encode(JSON.stringify(request));
|
|
if (encoded.byteLength > MAXIMUM_REQUEST_JSON_BYTES) {
|
|
throw new Error("The WebPerl request exceeds its JSON byte limit.");
|
|
}
|
|
return encoded;
|
|
}
|
|
|
|
function decodeUtf8Exact(encoded, label) {
|
|
var decoded;
|
|
try {
|
|
decoded = new TextDecoder("utf-8", { fatal: true }).decode(encoded);
|
|
} catch (_error) {
|
|
throw new Error(label + " is not valid UTF-8.");
|
|
}
|
|
var roundTrip = utf8Encoder.encode(decoded);
|
|
if (roundTrip.byteLength !== encoded.byteLength) {
|
|
throw new Error(label + " has an inconsistent UTF-8 length.");
|
|
}
|
|
for (var index = 0; index < encoded.byteLength; index += 1) {
|
|
if (roundTrip[index] !== encoded[index]) {
|
|
throw new Error(label + " is not canonical UTF-8.");
|
|
}
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
function readResponse() {
|
|
var encoded = FS.readFile(RESPONSE_PATH);
|
|
if (
|
|
Object.prototype.toString.call(encoded) !== "[object Uint8Array]" ||
|
|
encoded.byteLength === 0 ||
|
|
encoded.byteLength > MAXIMUM_RESPONSE_JSON_BYTES
|
|
) {
|
|
throw new Error("The fixed Perl bridge response exceeds its byte limit.");
|
|
}
|
|
var parsed;
|
|
try {
|
|
parsed = JSON.parse(decodeUtf8Exact(encoded, "The WebPerl response"));
|
|
} catch (error) {
|
|
if (error instanceof SyntaxError) {
|
|
throw new Error("The fixed Perl bridge returned malformed JSON.");
|
|
}
|
|
throw error;
|
|
}
|
|
if (!isRecord(parsed)) {
|
|
throw new Error("The fixed Perl bridge returned a non-object response.");
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function injectReplacementOutput(response, maximumOutputBytes) {
|
|
if (
|
|
owns(response, "output") ||
|
|
!isIntegerInRange(response.outputBytes, 0, maximumOutputBytes) ||
|
|
typeof response.outputTruncated !== "boolean"
|
|
) {
|
|
throw new Error("The fixed Perl bridge returned invalid output metadata.");
|
|
}
|
|
var encoded = FS.readFile(OUTPUT_PATH);
|
|
if (
|
|
Object.prototype.toString.call(encoded) !== "[object Uint8Array]" ||
|
|
encoded.byteLength !== response.outputBytes ||
|
|
encoded.byteLength > maximumOutputBytes
|
|
) {
|
|
throw new Error(
|
|
"The fixed Perl bridge output file exceeds its bounded contract.",
|
|
);
|
|
}
|
|
response.output = decodeUtf8Exact(encoded, "The WebPerl output file");
|
|
}
|
|
|
|
function removeFixedFile(path) {
|
|
try {
|
|
FS.unlink(path);
|
|
} catch (_error) {
|
|
// The fixed file does not exist before its first use.
|
|
}
|
|
}
|
|
|
|
var Perl = {
|
|
trace: false,
|
|
glue: function () {
|
|
throw new Error("The WebPerl JavaScript bridge is disabled.");
|
|
},
|
|
unglue: function () {
|
|
throw new Error("The WebPerl JavaScript bridge is disabled.");
|
|
},
|
|
};
|
|
|
|
var Module = {
|
|
noInitialRun: true,
|
|
noExitRuntime: true,
|
|
arguments: ["--version"],
|
|
print: function () {},
|
|
printErr: captureStderr,
|
|
stdout: function () {},
|
|
stderr: function (character) {
|
|
if (character) {
|
|
stderrTail = (stderrTail + String.fromCharCode(character)).slice(-4096);
|
|
}
|
|
},
|
|
stdin: function () {
|
|
return null;
|
|
},
|
|
locateFile: function (file) {
|
|
return new URL(file, runtimeRoot).href;
|
|
},
|
|
preRun: [],
|
|
onRuntimeInitialized: function () {
|
|
runtimeResolve();
|
|
},
|
|
onAbort: function (reason) {
|
|
runtimeReject(
|
|
new Error("The legacy WebPerl runtime aborted: " + String(reason)),
|
|
);
|
|
},
|
|
};
|
|
|
|
importScripts(new URL("emperl.js", runtimeRoot).href);
|
|
|
|
var harnessPromise;
|
|
|
|
async function loadHarness() {
|
|
if (harnessPromise) return harnessPromise;
|
|
harnessPromise = (async function () {
|
|
await runtimeReady;
|
|
var response = await fetch(new URL("regex_tools_bridge.pl", runtimeRoot));
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
"The fixed Perl bridge request failed with HTTP " +
|
|
response.status +
|
|
".",
|
|
);
|
|
}
|
|
var source = await response.text();
|
|
FS.writeFile(HARNESS_PATH, source, { encoding: "utf8" });
|
|
Module.callMain([HARNESS_PATH]);
|
|
return true;
|
|
})();
|
|
return harnessPromise;
|
|
}
|
|
|
|
async function invokeHarness(request) {
|
|
stderrTail = "";
|
|
await loadHarness();
|
|
if (stderrTail) {
|
|
throw new Error("The fixed Perl bridge failed to load: " + stderrTail);
|
|
}
|
|
removeFixedFile(REQUEST_PATH);
|
|
removeFixedFile(RESPONSE_PATH);
|
|
removeFixedFile(OUTPUT_PATH);
|
|
try {
|
|
FS.writeFile(REQUEST_PATH, encodeRequest(request));
|
|
var status = Module.ccall(
|
|
"webperl_eval_perl",
|
|
"string",
|
|
["string"],
|
|
[FIXED_INVOCATION],
|
|
);
|
|
if (status !== "ok") {
|
|
throw new Error("The fixed Perl bridge returned an invalid status.");
|
|
}
|
|
var parsed = readResponse();
|
|
if (request.operation === "replace" && parsed.ok === true) {
|
|
injectReplacementOutput(parsed, request.maximumOutputBytes);
|
|
} else if (
|
|
owns(parsed, "output") ||
|
|
owns(parsed, "outputBytes") ||
|
|
owns(parsed, "outputTruncated")
|
|
) {
|
|
throw new Error(
|
|
"The fixed Perl bridge returned output for a non-replacement result.",
|
|
);
|
|
}
|
|
return parsed;
|
|
} finally {
|
|
removeFixedFile(REQUEST_PATH);
|
|
removeFixedFile(RESPONSE_PATH);
|
|
removeFixedFile(OUTPUT_PATH);
|
|
}
|
|
}
|
|
|
|
async function handleMessage(message) {
|
|
if (
|
|
!message ||
|
|
message.protocolVersion !== BRIDGE_PROTOCOL_VERSION ||
|
|
!Number.isSafeInteger(message.requestId)
|
|
) {
|
|
throw new Error("The WebPerl bridge received an invalid message.");
|
|
}
|
|
if (message.kind === "load") {
|
|
return invokeHarness({ abi: 1, operation: "identity" });
|
|
}
|
|
if (message.kind === "run") {
|
|
if (
|
|
!message.request ||
|
|
typeof message.request !== "object" ||
|
|
Array.isArray(message.request)
|
|
) {
|
|
throw new Error("The WebPerl bridge received an invalid request.");
|
|
}
|
|
return invokeHarness(message.request);
|
|
}
|
|
throw new Error("The WebPerl bridge operation is unsupported.");
|
|
}
|
|
|
|
var operationQueue = Promise.resolve();
|
|
|
|
self.onmessage = function (event) {
|
|
var message = event.data;
|
|
var current = operationQueue.then(function () {
|
|
return handleMessage(message);
|
|
});
|
|
operationQueue = current.catch(function () {});
|
|
current.then(
|
|
function (payload) {
|
|
self.postMessage({
|
|
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
|
requestId: message && message.requestId,
|
|
ok: true,
|
|
payload: payload,
|
|
});
|
|
},
|
|
function (error) {
|
|
self.postMessage({
|
|
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
|
requestId: message && message.requestId,
|
|
ok: false,
|
|
error: {
|
|
name: error instanceof Error ? error.name : "Error",
|
|
message: boundedRuntimeError(error),
|
|
},
|
|
});
|
|
},
|
|
);
|
|
};
|