feat: add multi-engine regex flavour support
This commit is contained in:
367
public/engines/perl/perl-runtime.worker.js
Normal file
367
public/engines/perl/perl-runtime.worker.js
Normal file
@@ -0,0 +1,367 @@
|
||||
"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),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user