feat: add multi-engine regex flavour support

This commit is contained in:
2026-07-27 11:43:51 +02:00
parent 7079cde15f
commit 7f3a91ad37
340 changed files with 643286 additions and 483 deletions

View 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),
},
});
},
);
};