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