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