"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// packages/php-wasm/node/src/index.ts
var src_exports = {};
__export(src_exports, {
  FileLock: () => FileLock,
  FileLockManagerForNode: () => FileLockManagerForNode,
  createNodeFsMountHandler: () => createNodeFsMountHandler,
  getPHPLoaderModule: () => getPHPLoaderModule,
  loadNodeRuntime: () => loadNodeRuntime,
  useHostFilesystem: () => useHostFilesystem,
  withNetworking: () => withNetworking,
  withXdebug: () => withXdebug
});
module.exports = __toCommonJS(src_exports);
var import_node_polyfills = require("@php-wasm/node-polyfills");

// packages/php-wasm/node/src/lib/get-php-loader-module.ts
var import_universal = require("@php-wasm/universal");
async function getPHPLoaderModule(version = import_universal.LatestSupportedPHPVersion) {
  switch (version) {
    case "8.5":
      return (await import("@php-wasm/node-8-5")).getPHPLoaderModule();
    case "8.4":
      return (await import("@php-wasm/node-8-4")).getPHPLoaderModule();
    case "8.3":
      return (await import("@php-wasm/node-8-3")).getPHPLoaderModule();
    case "8.2":
      return (await import("@php-wasm/node-8-2")).getPHPLoaderModule();
    case "8.1":
      return (await import("@php-wasm/node-8-1")).getPHPLoaderModule();
    case "8.0":
      return (await import("@php-wasm/node-8-0")).getPHPLoaderModule();
    case "7.4":
      return (await import("@php-wasm/node-7-4")).getPHPLoaderModule();
    case "7.3":
      return (await import("@php-wasm/node-7-3")).getPHPLoaderModule();
    case "7.2":
      return (await import("@php-wasm/node-7-2")).getPHPLoaderModule();
  }
  throw new Error(`Unsupported PHP version ${version}`);
}

// packages/php-wasm/node/src/lib/networking/outbound-ws-to-tcp-proxy.ts
var dns = __toESM(require("dns"), 1);
var http = __toESM(require("http"), 1);
var net2 = __toESM(require("net"), 1);
var util = __toESM(require("node:util"), 1);
var import_ws = require("ws");

// packages/php-wasm/node/src/lib/networking/utils.ts
var net = __toESM(require("net"), 1);
var import_logger = require("@php-wasm/logger");
function debugLog(message, ...args) {
  if (process.env["DEV"] && !process.env["TEST"]) {
    import_logger.logger.log(message, ...args);
  }
}
async function findFreePorts(n) {
  const serversPromises = [];
  for (let i = 0; i < n; i++) {
    serversPromises.push(listenOnRandomPort());
  }
  const servers = await Promise.all(serversPromises);
  const ports = [];
  for (const server of servers) {
    const address = server.address();
    ports.push(address.port);
    server.close();
  }
  return ports;
}
function listenOnRandomPort() {
  return new Promise((resolve) => {
    const server = net.createServer();
    server.listen(0, () => {
      resolve(server);
    });
  });
}

// packages/php-wasm/node/src/lib/networking/outbound-ws-to-tcp-proxy.ts
function log(...args) {
  debugLog("[WS Server]", ...args);
}
var lookup2 = util.promisify(dns.lookup);
function prependByte(chunk, byte) {
  if (typeof chunk === "string") {
    chunk = String.fromCharCode(byte) + chunk;
  } else if (chunk instanceof ArrayBuffer || "byteLength" in chunk) {
    const buffer = new Uint8Array(chunk.byteLength + 1);
    buffer[0] = byte;
    buffer.set(new Uint8Array(chunk), 1);
    chunk = buffer.buffer;
  } else {
    log({ chunk });
    throw new Error("Unsupported chunk type: " + typeof chunk);
  }
  return chunk;
}
var COMMAND_CHUNK = 1;
var COMMAND_SET_SOCKETOPT = 2;
function addSocketOptionsSupportToWebSocketClass(WebSocketConstructor) {
  return class PHPWasmWebSocketConstructor extends WebSocketConstructor {
    // @ts-ignore
    send(chunk, callback) {
      return this.sendCommand(COMMAND_CHUNK, chunk, callback);
    }
    setSocketOpt(optionClass, optionName, optionValue) {
      return this.sendCommand(
        COMMAND_SET_SOCKETOPT,
        new Uint8Array([optionClass, optionName, optionValue]).buffer,
        () => void 0
      );
    }
    sendCommand(commandType, chunk, callback) {
      return WebSocketConstructor.prototype.send.call(
        this,
        prependByte(chunk, commandType),
        callback
      );
    }
  };
}
function initOutboundWebsocketProxyServer(listenPort, listenHost = "127.0.0.1") {
  log(`Binding the WebSockets server to ${listenHost}:${listenPort}...`);
  const webServer = http.createServer((request, response) => {
    response.writeHead(403, { "Content-Type": "text/plain" });
    response.write(
      "403 Permission Denied\nOnly websockets are allowed here.\n"
    );
    response.end();
  });
  return new Promise((resolve) => {
    webServer.listen(listenPort, listenHost, function() {
      const wsServer = new import_ws.WebSocketServer({ server: webServer });
      wsServer.on("connection", onWsConnect);
      resolve(webServer);
    });
  });
}
async function onWsConnect(client, request) {
  const clientAddr = client?._socket?.remoteAddress || client.url;
  const clientLog = function(...args) {
    log(" " + clientAddr + ": ", ...args);
  };
  clientLog(
    "WebSocket connection from : " + clientAddr + " at URL " + (request ? request.url : client.upgradeReq.url)
  );
  clientLog(
    "Version " + client.protocolVersion + ", subprotocol: " + client.protocol
  );
  const reqUrl = new URL(`ws://0.0.0.0` + request.url);
  const reqTargetPort = Number(reqUrl.searchParams.get("port"));
  const reqTargetHost = reqUrl.searchParams.get("host");
  if (!reqTargetPort || !reqTargetHost) {
    clientLog("Missing host or port information");
    client.close(3e3);
    return;
  }
  if (reqTargetPort < 0 || reqTargetPort > 65535) {
    clientLog("Invalid port number: " + reqTargetPort);
    client.send([]);
    client.close(3e3);
    return;
  }
  let target;
  const recvQueue = [];
  function flushMessagesQueue() {
    while (recvQueue.length > 0) {
      const msg = recvQueue.pop();
      const commandType = msg[0];
      clientLog("flushing", { commandType }, msg);
      if (commandType === COMMAND_CHUNK) {
        target.write(msg.slice(1));
      } else if (commandType === COMMAND_SET_SOCKETOPT) {
        const SOL_SOCKET = 1;
        const SO_KEEPALIVE = 9;
        const IPPROTO_TCP = 6;
        const TCP_NODELAY = 1;
        if (msg[1] === SOL_SOCKET && msg[2] === SO_KEEPALIVE) {
          target.setKeepAlive(msg[3]);
        } else if (msg[1] === IPPROTO_TCP && msg[2] === TCP_NODELAY) {
          target.setNoDelay(msg[3]);
        }
      } else {
        clientLog("Unknown command type: " + commandType);
        process.exit();
      }
    }
  }
  client.on("message", function(msg) {
    recvQueue.unshift(msg);
    if (target) {
      flushMessagesQueue();
    }
  });
  client.on("close", function(code, reason) {
    clientLog(
      "WebSocket client disconnected: " + code + " [" + reason + "]"
    );
    if (target) {
      target.end();
    }
  });
  client.on("error", function(a) {
    clientLog("WebSocket client error: " + a);
    target.end();
  });
  let reqTargetIp;
  if (net2.isIP(reqTargetHost) === 0) {
    clientLog("resolving " + reqTargetHost + "... ");
    try {
      const resolution = await lookup2(reqTargetHost);
      reqTargetIp = resolution.address;
      clientLog("resolved " + reqTargetHost + " -> " + reqTargetIp);
    } catch (e) {
      clientLog("can't resolve " + reqTargetHost + " due to:", e);
      client.send([]);
      setTimeout(() => {
        client.close(3e3);
      });
      return;
    }
  } else {
    reqTargetIp = reqTargetHost;
  }
  clientLog(
    "Opening a socket connection to " + reqTargetIp + ":" + reqTargetPort
  );
  target = net2.createConnection(reqTargetPort, reqTargetIp, function() {
    clientLog("Connected to target");
    flushMessagesQueue();
  });
  target.on("data", function(data) {
    try {
      client.send(data);
    } catch {
      clientLog("Client closed, cleaning up target");
      target.end();
    }
  });
  target.on("end", function() {
    clientLog("target disconnected");
    client.close();
  });
  target.on("error", function(e) {
    clientLog("target connection error", e);
    client.send([]);
    setTimeout(() => {
      client.close(3e3);
      try {
        target.end();
      } catch {
      }
    });
  });
}

// packages/php-wasm/node/src/lib/networking/inbound-tcp-to-ws-proxy.ts
var import_net = require("net");
var import_ws2 = require("ws");
function log2(...args) {
  debugLog("[TCP Server]", ...args);
}
function addTCPServerToWebSocketServerClass(wsListenPort, WSServer) {
  return class PHPWasmWebSocketServer extends WSServer {
    constructor(options, callback) {
      const requestedPort = options.port;
      options.port = wsListenPort;
      listenTCPToWSProxy({
        tcpListenPort: requestedPort,
        wsConnectPort: wsListenPort
      });
      super(options, callback);
    }
  };
}
function listenTCPToWSProxy(options) {
  options = {
    wsConnectHost: "127.0.0.1",
    ...options
  };
  const { tcpListenPort, wsConnectHost, wsConnectPort } = options;
  const server = (0, import_net.createServer)();
  server.on("connection", function handleConnection(tcpSource) {
    const inBuffer = [];
    const wsTarget = new import_ws2.WebSocket(
      `ws://${wsConnectHost}:${wsConnectPort}/`
    );
    wsTarget.binaryType = "arraybuffer";
    function wsSend(data) {
      wsTarget.send(new Uint8Array(data));
    }
    wsTarget.addEventListener("open", function() {
      log2("Outbound WebSocket connection established");
      while (inBuffer.length > 0) {
        wsSend(inBuffer.shift());
      }
    });
    wsTarget.addEventListener("message", (e) => {
      log2(
        "WS->TCP message:",
        new TextDecoder().decode(e.data)
      );
      tcpSource.write(Buffer.from(e.data));
    });
    wsTarget.addEventListener("close", () => {
      log2("WebSocket connection closed");
      tcpSource.end();
    });
    tcpSource.on("data", function(data) {
      log2("TCP->WS message:", data);
      if (wsTarget.readyState === import_ws2.WebSocket.OPEN) {
        while (inBuffer.length > 0) {
          wsSend(inBuffer.shift());
        }
        wsSend(data);
      } else {
        inBuffer.push(data);
      }
    });
    tcpSource.once("close", function() {
      log2("TCP connection closed");
      wsTarget.close();
    });
    tcpSource.on("error", function() {
      log2("TCP connection error");
      wsTarget.close();
    });
  });
  server.listen(tcpListenPort, function() {
    log2("TCP server listening");
  });
}

// packages/php-wasm/node/src/lib/networking/with-networking.ts
async function withNetworking(phpModuleArgs = {}) {
  const [inboundProxyWsServerPort, outboundProxyWsServerPort] = await findFreePorts(2);
  const outboundNetworkProxyServer = await initOutboundWebsocketProxyServer(
    outboundProxyWsServerPort
  );
  return {
    ...phpModuleArgs,
    outboundNetworkProxyServer,
    websocket: {
      ...phpModuleArgs["websocket"] || {},
      url: (_, host, port) => {
        const query = new URLSearchParams({
          host,
          port
        }).toString();
        return `ws://127.0.0.1:${outboundProxyWsServerPort}/?${query}`;
      },
      subprotocol: "binary",
      decorator: addSocketOptionsSupportToWebSocketClass,
      serverDecorator: addTCPServerToWebSocketServerClass.bind(
        null,
        inboundProxyWsServerPort
      )
    }
  };
}

// packages/php-wasm/node/src/lib/load-runtime.ts
var import_universal6 = require("@php-wasm/universal");
var import_fs3 = __toESM(require("fs"), 1);

// packages/php-wasm/node/src/lib/extensions/xdebug/with-xdebug.ts
var import_universal3 = require("@php-wasm/universal");
var import_fs = __toESM(require("fs"), 1);

// packages/php-wasm/node/src/lib/extensions/xdebug/get-xdebug-extension-module.ts
var import_universal2 = require("@php-wasm/universal");
async function getXdebugExtensionModule(version = import_universal2.LatestSupportedPHPVersion) {
  switch (version) {
    case "8.5":
      return (await import("@php-wasm/node-8-5")).getXdebugExtensionPath();
    case "8.4":
      return (await import("@php-wasm/node-8-4")).getXdebugExtensionPath();
    case "8.3":
      return (await import("@php-wasm/node-8-3")).getXdebugExtensionPath();
    case "8.2":
      return (await import("@php-wasm/node-8-2")).getXdebugExtensionPath();
    case "8.1":
      return (await import("@php-wasm/node-8-1")).getXdebugExtensionPath();
    case "8.0":
      return (await import("@php-wasm/node-8-0")).getXdebugExtensionPath();
    case "7.4":
      return (await import("@php-wasm/node-7-4")).getXdebugExtensionPath();
    case "7.3":
      return (await import("@php-wasm/node-7-3")).getXdebugExtensionPath();
    case "7.2":
      return (await import("@php-wasm/node-7-2")).getXdebugExtensionPath();
  }
  throw new Error(`Unsupported PHP version ${version}`);
}

// packages/php-wasm/node/src/lib/extensions/xdebug/with-xdebug.ts
async function withXdebug(version = import_universal3.LatestSupportedPHPVersion, options, xdebugOptions) {
  const fileName = "xdebug.so";
  const filePath = await getXdebugExtensionModule(version);
  const extension = import_fs.default.readFileSync(filePath);
  return {
    ...options,
    ENV: {
      ...options.ENV,
      PHP_INI_SCAN_DIR: "/internal/shared/extensions"
    },
    onRuntimeInitialized: (phpRuntime) => {
      if (options.onRuntimeInitialized) {
        options.onRuntimeInitialized(phpRuntime);
      }
      if (!import_universal3.FSHelpers.fileExists(
        phpRuntime.FS,
        "/internal/shared/extensions"
      )) {
        phpRuntime.FS.mkdirTree("/internal/shared/extensions");
      }
      if (!import_universal3.FSHelpers.fileExists(
        phpRuntime.FS,
        `/internal/shared/extensions/${fileName}`
      )) {
        phpRuntime.FS.writeFile(
          `/internal/shared/extensions/${fileName}`,
          new Uint8Array(extension)
        );
      }
      if (!import_universal3.FSHelpers.fileExists(
        phpRuntime.FS,
        "/internal/shared/extensions/xdebug.ini"
      )) {
        const ideKey = xdebugOptions?.ideKey || "PLAYGROUNDCLI";
        phpRuntime.FS.writeFile(
          "/internal/shared/extensions/xdebug.ini",
          [
            "zend_extension=/internal/shared/extensions/xdebug.so",
            "xdebug.mode=debug,develop",
            "xdebug.start_with_request=yes",
            `xdebug.idekey="${ideKey}"`
          ].join("\n")
        );
      }
      phpRuntime.FS.mkdirTree(process.cwd());
      phpRuntime.FS.mount(
        phpRuntime.FS.filesystems["NODEFS"],
        { root: process.cwd() },
        process.cwd()
      );
      phpRuntime.FS.chdir(process.cwd());
    }
  };
}

// packages/php-wasm/node/src/lib/extensions/intl/with-intl.ts
var import_universal5 = require("@php-wasm/universal");
var import_fs2 = __toESM(require("fs"), 1);

// packages/php-wasm/node/src/lib/extensions/intl/get-intl-extension-module.ts
var import_universal4 = require("@php-wasm/universal");
async function getIntlExtensionModule(version = import_universal4.LatestSupportedPHPVersion) {
  switch (version) {
    case "8.5":
      return (await import("@php-wasm/node-8-5")).getIntlExtensionPath();
    case "8.4":
      return (await import("@php-wasm/node-8-4")).getIntlExtensionPath();
    case "8.3":
      return (await import("@php-wasm/node-8-3")).getIntlExtensionPath();
    case "8.2":
      return (await import("@php-wasm/node-8-2")).getIntlExtensionPath();
    case "8.1":
      return (await import("@php-wasm/node-8-1")).getIntlExtensionPath();
    case "8.0":
      return (await import("@php-wasm/node-8-0")).getIntlExtensionPath();
    case "7.4":
      return (await import("@php-wasm/node-7-4")).getIntlExtensionPath();
    case "7.3":
      return (await import("@php-wasm/node-7-3")).getIntlExtensionPath();
    case "7.2":
      return (await import("@php-wasm/node-7-2")).getIntlExtensionPath();
  }
  throw new Error(`Unsupported PHP version ${version}`);
}

// packages/php-wasm/node/src/lib/extensions/intl/with-intl.ts
async function withIntl(version = import_universal5.LatestSupportedPHPVersion, options) {
  const extensionName = "intl.so";
  const extensionPath = await getIntlExtensionModule(version);
  const extension = import_fs2.default.readFileSync(extensionPath);
  const dataName = "icu.dat";
  const dataPath = `${__dirname}/shared/${dataName}`;
  const ICUData = import_fs2.default.readFileSync(dataPath);
  return {
    ...options,
    ENV: {
      ...options.ENV,
      PHP_INI_SCAN_DIR: "/internal/shared/extensions",
      ICU_DATA: "/internal/shared"
    },
    onRuntimeInitialized: (phpRuntime) => {
      if (options.onRuntimeInitialized) {
        options.onRuntimeInitialized(phpRuntime);
      }
      if (!import_universal5.FSHelpers.fileExists(
        phpRuntime.FS,
        "/internal/shared/extensions"
      )) {
        phpRuntime.FS.mkdirTree("/internal/shared/extensions");
      }
      if (!import_universal5.FSHelpers.fileExists(
        phpRuntime.FS,
        `/internal/shared/extensions/${extensionName}`
      )) {
        phpRuntime.FS.writeFile(
          `/internal/shared/extensions/${extensionName}`,
          new Uint8Array(extension)
        );
      }
      if (!import_universal5.FSHelpers.fileExists(
        phpRuntime.FS,
        "/internal/shared/extensions/intl.ini"
      )) {
        phpRuntime.FS.writeFile(
          "/internal/shared/extensions/intl.ini",
          [
            `extension=/internal/shared/extensions/${extensionName}`
          ].join("\n")
        );
      }
      if (!import_universal5.FSHelpers.fileExists(
        phpRuntime.FS,
        `${phpRuntime.ENV.ICU_DATA}/${dataName}`
      )) {
        phpRuntime.FS.mkdirTree(phpRuntime.ENV.ICU_DATA);
        phpRuntime.FS.writeFile(
          `${phpRuntime.ENV.ICU_DATA}/icudt74l.dat`,
          new Uint8Array(ICUData)
        );
      }
    }
  };
}

// packages/php-wasm/node/src/lib/load-runtime.ts
var import_util = require("@php-wasm/util");
var import_path = require("path");
async function loadNodeRuntime(phpVersion, options = {}) {
  let emscriptenOptions = {
    /**
     * Emscripten default behavior is to kill the process when
     * the WASM program calls `exit()`. We want to throw an
     * exception instead.
     */
    quit: function(code, error) {
      throw error;
    },
    ...options.emscriptenOptions || {},
    onRuntimeInitialized: (phpRuntime) => {
      if (options?.followSymlinks === true) {
        phpRuntime.FS.filesystems.NODEFS.node_ops.readlink = (node) => {
          const absoluteSourcePath = phpRuntime.FS.filesystems.NODEFS.tryFSOperation(
            () => import_fs3.default.realpathSync(
              phpRuntime.FS.filesystems.NODEFS.realPath(node)
            )
          );
          const symlinkMountPath = (0, import_util.joinPaths)(
            `/internal/symlinks`,
            absoluteSourcePath
          );
          if (import_fs3.default.existsSync(absoluteSourcePath)) {
            if (!import_universal6.FSHelpers.fileExists(
              phpRuntime.FS,
              symlinkMountPath
            )) {
              const sourceStat = import_fs3.default.statSync(absoluteSourcePath);
              if (sourceStat.isDirectory()) {
                phpRuntime.FS.mkdirTree(symlinkMountPath);
              } else if (sourceStat.isFile()) {
                phpRuntime.FS.mkdirTree(
                  (0, import_path.dirname)(symlinkMountPath)
                );
                phpRuntime.FS.writeFile(symlinkMountPath, "");
              } else {
                throw new Error(
                  "Unsupported file type. PHP-wasm supports only symlinks that link to files, directories, or symlinks."
                );
              }
            }
            const symlinkMountNode = phpRuntime.FS.lookupPath(symlinkMountPath).node;
            const isSymlinkMounted = symlinkMountNode.mount.mountpoint === symlinkMountPath;
            if (!isSymlinkMounted) {
              phpRuntime.FS.mount(
                phpRuntime.FS.filesystems.NODEFS,
                { root: absoluteSourcePath },
                symlinkMountPath
              );
            }
          }
          return symlinkMountPath;
        };
      }
      phpRuntime.FS.root.node_ops = {
        ...phpRuntime.FS.root.node_ops,
        statfs: phpRuntime.FS.filesystems.NODEFS.node_ops.statfs
      };
      phpRuntime.FS.root.mount.opts.root = ".";
    }
  };
  if (options?.withXdebug === true) {
    emscriptenOptions = await withXdebug(
      phpVersion,
      emscriptenOptions,
      options.xdebug
    );
  }
  if (options?.withIntl === true) {
    emscriptenOptions = await withIntl(phpVersion, emscriptenOptions);
  }
  emscriptenOptions = await withNetworking(emscriptenOptions);
  return await (0, import_universal6.loadPHPRuntime)(
    await getPHPLoaderModule(phpVersion),
    emscriptenOptions
  );
}

// packages/php-wasm/node/src/lib/use-host-filesystem.ts
var import_node_fs = require("node:fs");

// packages/php-wasm/node/src/lib/node-fs-mount.ts
var import_universal7 = require("@php-wasm/universal");
var import_util2 = require("@php-wasm/util");
var import_fs4 = require("fs");
var import_path2 = require("path");
function createNodeFsMountHandler(localPath) {
  return function(php, FS, vfsMountPoint) {
    let removeVfsNode = false;
    if (!import_universal7.FSHelpers.fileExists(FS, vfsMountPoint)) {
      const lstat = (0, import_fs4.lstatSync)(localPath);
      if (lstat.isFile() || lstat.isSymbolicLink()) {
        FS.mkdirTree((0, import_path2.dirname)(vfsMountPoint));
        FS.writeFile(vfsMountPoint, "");
      } else if (lstat.isDirectory()) {
        FS.mkdirTree(vfsMountPoint);
      } else {
        throw new Error(
          "Unsupported file type. PHP-wasm supports only symlinks that link to files, directories, or symlinks."
        );
      }
      removeVfsNode = true;
    }
    let lookup3;
    try {
      lookup3 = FS.lookupPath(vfsMountPoint);
    } catch (e) {
      const error = e;
      if (error.errno === 44) {
        throw new Error(
          `Unable to access the mount point ${vfsMountPoint} in VFS after attempting to create it.`
        );
      }
      throw e;
    }
    FS.mount(FS.filesystems["NODEFS"], { root: localPath }, vfsMountPoint);
    return () => {
      FS.unmount(vfsMountPoint);
      if (removeVfsNode) {
        if (FS.isDir(lookup3.node.mode)) {
          if ((0, import_util2.isParentOf)(vfsMountPoint, FS.cwd())) {
            throw new Error(
              `Cannot remove the VFS directory "${vfsMountPoint}" on umount cleanup \u2013 it is a parent of the CWD "${FS.cwd()}". Change CWD before unmounting or explicitly disable post-unmount node cleanup with createNodeFsMountHandler(path, {cleanupNodesOnUnmount: false}).`
            );
          }
          FS.rmdir(vfsMountPoint);
        } else {
          FS.unlink(vfsMountPoint);
        }
      }
    };
  };
}

// packages/php-wasm/node/src/lib/use-host-filesystem.ts
function useHostFilesystem(php) {
  const dirs = (0, import_node_fs.readdirSync)("/").filter((file) => file !== "dev").filter((file) => file !== "proc").map((file) => `/${file}`).filter((file) => {
    try {
      return statPathFollowSymlinks(file).isDirectory();
    } catch {
      return false;
    }
  });
  for (const dir of dirs) {
    if (!php.fileExists(dir)) {
      php.mkdirTree(dir);
    }
    php.mount(dir, createNodeFsMountHandler(dir));
  }
  php.chdir(process.cwd());
}
function statPathFollowSymlinks(path) {
  let stat = (0, import_node_fs.lstatSync)(path);
  if (stat.isSymbolicLink()) {
    const fs4 = require("fs");
    let target = path;
    const seen = /* @__PURE__ */ new Set();
    while (true) {
      if (seen.has(target)) {
        throw new Error(`Symlink loop detected: ${path}`);
      }
      seen.add(target);
      const linkStat = (0, import_node_fs.lstatSync)(target);
      if (linkStat.isSymbolicLink()) {
        target = fs4.realpathSync(target);
        continue;
      }
      stat = linkStat;
      break;
    }
  }
  return stat;
}

// packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts
var import_logger2 = require("@php-wasm/logger");
var import_fs5 = require("fs");
var MAX_64BIT_OFFSET = BigInt(2n ** 64n - 1n);
var FileLockManagerForNode = class {
  /**
   * Create a new FileLockManagerForNode instance.
   *
   * @param nativeFlockSync A synchronous flock() function to lock files via the host OS.
   */
  constructor(nativeFlockSync = function flockSyncNoOp() {
  }) {
    this.nativeFlockSync = nativeFlockSync;
    this.locks = /* @__PURE__ */ new Map();
  }
  /**
   * Lock the whole file.
   *
   * @param path The path to the file to lock. This should be the path
   *             of the file in the native filesystem.
   * @param op The whole file lock operation to perform.
   * @returns True if the lock was granted, false otherwise.
   */
  lockWholeFile(path, op) {
    if (this.locks.get(path) === void 0) {
      if (op.type === "unlock") {
        return true;
      }
      const maybeLock = FileLock.maybeCreate(
        path,
        op.type,
        this.nativeFlockSync
      );
      if (maybeLock === void 0) {
        return false;
      }
      this.locks.set(path, maybeLock);
    }
    const lock = this.locks.get(path);
    const result = lock.lockWholeFile(op);
    this.forgetPathIfUnlocked(path);
    return result;
  }
  /**
   * Lock a byte range.
   *
   * @param path The path to the file to lock. This should be the path
   *             of the file in the native filesystem.
   * @param requestedLock The byte range lock to perform.
   * @returns True if the lock was granted, false otherwise.
   */
  lockFileByteRange(path, requestedLock) {
    if (!this.locks.has(path)) {
      if (requestedLock.type === "unlocked") {
        return true;
      }
      const maybeLock = FileLock.maybeCreate(
        path,
        requestedLock.type,
        this.nativeFlockSync
      );
      if (maybeLock === void 0) {
        return false;
      }
      this.locks.set(path, maybeLock);
    }
    const lock = this.locks.get(path);
    return lock.lockFileByteRange(requestedLock);
  }
  /**
   * Find the first conflicting byte range lock.
   *
   * @param path The path to the file to find the conflicting lock for.
   * @param desiredLock The desired byte range lock.
   * @returns The first conflicting byte range lock, or undefined if no conflicting lock exists.
   */
  findFirstConflictingByteRangeLock(path, desiredLock) {
    const lock = this.locks.get(path);
    if (lock === void 0) {
      return void 0;
    }
    return lock.findFirstConflictingByteRangeLock(desiredLock);
  }
  /**
   * Release all locks for the given process.
   *
   * @param pid The process ID to release locks for.
   */
  releaseLocksForProcess(pid) {
    for (const [path, lock] of this.locks.entries()) {
      lock.releaseLocksForProcess(pid);
      this.forgetPathIfUnlocked(path);
    }
  }
  /**
   * Release all locks for the given process and file descriptor.
   *
   * @param pid The process ID to release locks for.
   * @param fd The file descriptor to release locks for.
   * @param path The path to the file to release locks for.
   */
  releaseLocksForProcessFd(pid, fd, nativePath) {
    const lock = this.locks.get(nativePath);
    if (!lock) {
      return;
    }
    lock.releaseLocksForProcessFd(pid, fd);
    this.forgetPathIfUnlocked(nativePath);
  }
  /**
   * Forget the path if it is unlocked.
   *
   * @param path The path to the file to forget.
   */
  forgetPathIfUnlocked(path) {
    const lock = this.locks.get(path);
    if (!lock) {
      return;
    }
    if (lock.isUnlocked()) {
      lock.dispose();
      this.locks.delete(path);
    }
  }
};
var FileLock = class _FileLock {
  /**
   * Create a new FileLock instance for the given file and mode.
   * Fail if the underlying native file lock cannot be acquired.
   *
   * @param path The path to the file to lock
   * @param mode The type of lock to acquire
   * @returns A FileLock instance if the lock was acquired, undefined otherwise
   */
  static maybeCreate(path, mode, nativeFlockSync) {
    let fd;
    try {
      fd = (0, import_fs5.openSync)(path, "a+");
      const flockFlags = mode === "exclusive" ? "exnb" : "shnb";
      nativeFlockSync(fd, flockFlags);
      const nativeLock = { fd, mode, nativeFlockSync };
      return new _FileLock(nativeLock);
    } catch {
      if (fd !== void 0) {
        try {
          (0, import_fs5.closeSync)(fd);
        } catch (error) {
          import_logger2.logger.error(
            "Error closing locking file descriptor",
            error
          );
        }
      }
      return void 0;
    }
  }
  constructor(nativeLock) {
    this.nativeLock = nativeLock;
    this.rangeLocks = new FileLockIntervalTree();
    this.wholeFileLock = { type: "unlocked" };
  }
  /**
   * Close the file descriptor and release the native lock.
   *
   * @TODO Replace this with a Symbol.dispose property once supported by all JS runtimes.
   */
  dispose() {
    try {
      (0, import_fs5.closeSync)(this.nativeLock.fd);
    } catch (error) {
      import_logger2.logger.error("Error closing locking file descriptor", error);
    }
  }
  /**
   * Lock the whole file.
   *
   * This method corresponds to the flock() function.
   *
   * @param op The whole file lock operation to perform.
   * @returns True if the lock was granted, false otherwise.
   */
  lockWholeFile(op) {
    if (op.type === "unlock") {
      const originalType = this.wholeFileLock.type;
      if (originalType === "unlocked") {
      } else if (this.wholeFileLock.type === "exclusive" && this.wholeFileLock.pid === op.pid && this.wholeFileLock.fd === op.fd) {
        this.wholeFileLock = { type: "unlocked" };
      } else if (this.wholeFileLock.type === "shared" && this.wholeFileLock.pidFds.has(op.pid) && this.wholeFileLock.pidFds.get(op.pid).has(op.fd)) {
        this.wholeFileLock.pidFds.get(op.pid).delete(op.fd);
        if (this.wholeFileLock.pidFds.get(op.pid).size === 0) {
          this.wholeFileLock.pidFds.delete(op.pid);
        }
        if (this.wholeFileLock.pidFds.size === 0) {
          this.wholeFileLock = { type: "unlocked" };
        }
      }
      if (!this.ensureCompatibleNativeLock()) {
        import_logger2.logger.error(
          "Unable to update native lock after removing a whole file lock."
        );
      }
      return true;
    }
    if (this.isThereAConflictWithRequestedWholeFileLock(op)) {
      return false;
    }
    if (!this.ensureCompatibleNativeLock({
      overrideWholeFileLockType: op.type
    })) {
      return false;
    }
    if (op.type === "exclusive") {
      this.wholeFileLock = {
        type: "exclusive",
        pid: op.pid,
        fd: op.fd
      };
      return true;
    }
    if (op.type === "shared") {
      if (this.wholeFileLock.type !== "shared") {
        this.wholeFileLock = {
          type: "shared",
          pidFds: /* @__PURE__ */ new Map()
        };
      }
      const sharedLock = this.wholeFileLock;
      if (!sharedLock.pidFds.has(op.pid)) {
        sharedLock.pidFds.set(op.pid, /* @__PURE__ */ new Set());
      }
      sharedLock.pidFds.get(op.pid).add(op.fd);
      return true;
    }
    throw new Error(`Unexpected wholeFileLock() op: '${op.type}'`);
  }
  /**
   * Lock a byte range.
   *
   * This method corresponds to the fcntl() F_SETLK command.
   *
   * @param requestedLock The byte range lock to perform.
   * @returns True if the lock was granted, false otherwise.
   */
  lockFileByteRange(requestedLock) {
    if (requestedLock.start === requestedLock.end) {
      requestedLock = {
        ...requestedLock,
        end: MAX_64BIT_OFFSET
      };
    }
    if (requestedLock.type === "unlocked") {
      const overlappingLocksBySameProcess = this.rangeLocks.findOverlapping(requestedLock).filter((lock) => lock.pid === requestedLock.pid);
      for (const overlappingLock of overlappingLocksBySameProcess) {
        this.rangeLocks.remove(overlappingLock);
        if (overlappingLock.start < requestedLock.start) {
          this.rangeLocks.insert({
            ...overlappingLock,
            end: requestedLock.start
          });
        }
        if (overlappingLock.end > requestedLock.end) {
          this.rangeLocks.insert({
            ...overlappingLock,
            start: requestedLock.end
          });
        }
      }
      if (!this.ensureCompatibleNativeLock()) {
        import_logger2.logger.error(
          "Unable to update native lock after removing a byte range lock."
        );
      }
      return true;
    }
    if (this.isThereAConflictWithRequestedRangeLock(requestedLock)) {
      return false;
    }
    if (!this.ensureCompatibleNativeLock({
      overrideRangeLockType: requestedLock.type
    })) {
      return false;
    }
    const overlappingLocksFromSameProcess = this.rangeLocks.findOverlapping(requestedLock).filter((lock) => lock.pid === requestedLock.pid);
    let minStart = requestedLock.start;
    let maxEnd = requestedLock.end;
    for (const overlappingLock of overlappingLocksFromSameProcess) {
      this.rangeLocks.remove(overlappingLock);
      if (overlappingLock.start < minStart) {
        minStart = overlappingLock.start;
      }
      if (overlappingLock.end > maxEnd) {
        maxEnd = overlappingLock.end;
      }
    }
    const mergedLock = {
      ...requestedLock,
      start: minStart,
      end: maxEnd
    };
    this.rangeLocks.insert(mergedLock);
    return true;
  }
  /**
   * Find the first conflicting byte range lock.
   *
   * This method corresponds to the fcntl() F_GETLK command.
   *
   * @param desiredLock The desired byte range lock.
   * @returns The first conflicting byte range lock, or undefined if no conflicting lock exists.
   */
  findFirstConflictingByteRangeLock(desiredLock) {
    const overlappingLocks = this.rangeLocks.findOverlapping(desiredLock);
    const firstConflictingRangeLock = overlappingLocks.find(
      (lock) => lock.pid !== desiredLock.pid && (desiredLock.type === "exclusive" || lock.type === "exclusive")
    );
    if (firstConflictingRangeLock) {
      return firstConflictingRangeLock;
    }
    if (this.wholeFileLock.type === "unlocked") {
      return void 0;
    }
    const wfl = this.wholeFileLock;
    if (wfl.type === "exclusive" || desiredLock.type === "exclusive") {
      return {
        type: this.wholeFileLock.type,
        start: 0n,
        end: 0n,
        pid: -1
      };
    }
    return void 0;
  }
  /**
   * Release all locks for the given process.
   *
   * @param pid The process ID to release locks for.
   */
  releaseLocksForProcess(pid) {
    for (const rangeLock of this.rangeLocks.findLocksForProcess(pid)) {
      this.lockFileByteRange({
        ...rangeLock,
        type: "unlocked"
      });
    }
    if (this.wholeFileLock.type === "exclusive" && this.wholeFileLock.pid === pid) {
      this.lockWholeFile({
        pid,
        fd: this.wholeFileLock.fd,
        type: "unlock"
      });
    } else if (this.wholeFileLock.type === "shared" && this.wholeFileLock.pidFds.has(pid)) {
      for (const fd of this.wholeFileLock.pidFds.get(pid)) {
        this.lockWholeFile({
          pid,
          fd,
          type: "unlock"
        });
      }
    }
  }
  /**
   * Release all locks for the given process and file descriptor.
   *
   * @param pid The process ID to release locks for.
   * @param fd The file descriptor to release locks for.
   */
  releaseLocksForProcessFd(pid, fd) {
    for (const rangeLock of this.rangeLocks.findLocksForProcess(pid)) {
      this.lockFileByteRange({
        ...rangeLock,
        type: "unlocked"
      });
    }
    this.lockWholeFile({
      pid,
      fd,
      type: "unlock"
    });
  }
  /**
   * Check if the file lock is unlocked.
   *
   * @returns True if the file lock is unlocked, false otherwise.
   */
  isUnlocked() {
    return this.wholeFileLock.type === "unlocked" && this.rangeLocks.isEmpty();
  }
  /**
   * Ensure that the native lock is compatible with the php-wasm lock,
   * upgrading or downgrading as needed.
   *
   * @param overrideWholeFileLockType If provided, use this type for the whole file lock.
   * @param overrideRangeLockType If provided, use this type for the range lock.
   * @returns True if the native lock was upgraded or downgraded, false otherwise.
   */
  ensureCompatibleNativeLock({
    overrideWholeFileLockType,
    overrideRangeLockType
  } = {}) {
    const wholeFileLockType = overrideWholeFileLockType ?? this.wholeFileLock.type;
    const rangeLockType = overrideRangeLockType ?? this.rangeLocks.findStrictestExistingLockType();
    let requiredNativeLockType;
    if (wholeFileLockType === "exclusive" || rangeLockType === "exclusive") {
      requiredNativeLockType = "exclusive";
    } else if (wholeFileLockType === "shared" || rangeLockType === "shared") {
      requiredNativeLockType = "shared";
    } else {
      requiredNativeLockType = "unlock";
    }
    if (this.nativeLock.mode === requiredNativeLockType) {
      return true;
    }
    const flockFlags = requiredNativeLockType === "exclusive" && "exnb" || requiredNativeLockType === "shared" && "shnb" || "un";
    try {
      this.nativeLock.nativeFlockSync(this.nativeLock.fd, flockFlags);
      this.nativeLock.mode = requiredNativeLockType;
      return true;
    } catch {
      return false;
    }
  }
  /**
   * Check if a lock exists that conflicts with the requested range lock.
   *
   * @param requestedLock The desired byte range lock.
   * @returns True if a conflicting lock exists, false otherwise.
   */
  isThereAConflictWithRequestedRangeLock(requestedLock) {
    return this.findFirstConflictingByteRangeLock(requestedLock) !== void 0;
  }
  /**
   * Check if a lock exists that conflicts with the requested whole-file lock.
   *
   * @param requestedLock The desired whole-file lock.
   * @returns True if a conflicting lock exists, false otherwise.
   */
  isThereAConflictWithRequestedWholeFileLock(requestedLock) {
    if (requestedLock.type === "exclusive") {
      if (this.wholeFileLock.type === "exclusive" && (this.wholeFileLock.fd !== requestedLock.fd || this.wholeFileLock.pid !== requestedLock.pid)) {
        return true;
      }
      if (this.wholeFileLock.type === "shared" && Array.from(this.wholeFileLock.pidFds).some(
        ([pid]) => pid !== requestedLock.pid
      )) {
        return true;
      }
      const overlappingLocks = this.rangeLocks.findOverlapping({
        type: "unlocked",
        start: 0n,
        end: MAX_64BIT_OFFSET,
        pid: -1
      });
      if (overlappingLocks.length > 0) {
        return true;
      }
      return false;
    }
    if (requestedLock.type === "shared") {
      if (this.wholeFileLock.type === "exclusive" && this.wholeFileLock.pid !== requestedLock.pid) {
        return true;
      }
      const overlappingLocks = this.rangeLocks.findOverlapping({
        type: "unlocked",
        start: 0n,
        end: MAX_64BIT_OFFSET,
        pid: -1
      });
      const exclusiveRangeLocks = overlappingLocks.filter(
        (lock) => lock.type === "exclusive"
      );
      if (exclusiveRangeLocks.length > 0) {
        return true;
      }
      return false;
    }
    return false;
  }
};
var IntervalNode = class {
  constructor(range) {
    this.left = null;
    this.right = null;
    this.range = range;
    this.max = range.end;
  }
};
var FileLockIntervalTree = class {
  constructor() {
    this.root = null;
  }
  isEmpty() {
    return this.root === null;
  }
  /**
   * Insert a new locked range into the tree
   */
  insert(range) {
    this.root = this.insertNode(this.root, range);
  }
  /**
   * Find all ranges that overlap with the given range
   */
  findOverlapping(range) {
    const result = [];
    this.findOverlappingRanges(this.root, range, result);
    return result;
  }
  /**
   * Remove a lock range from the tree
   */
  remove(range) {
    this.root = this.removeNode(this.root, range);
  }
  /**
   * Find all ranges locked by the given process.
   *
   * @param pid The process ID to find locks for.
   * @returns All locked ranges for the given process.
   */
  findLocksForProcess(pid) {
    const result = [];
    this.findLocksForProcessInNode(this.root, pid, result);
    return result;
  }
  /**
   * Find the strictest existing lock type in the range lock tree.
   *
   * @returns The strictest existing lock type, or 'unlocked' if no locks exist.
   */
  findStrictestExistingLockType() {
    let maxType = "unlocked";
    const traverse = (node) => {
      if (!node) {
        return;
      }
      if (node.range.type === "exclusive") {
        maxType = "exclusive";
        return;
      }
      if (node.range.type === "shared") {
        maxType = "shared";
      }
      traverse(node.left);
      traverse(node.right);
    };
    traverse(this.root);
    return maxType;
  }
  insertNode(node, range) {
    if (!node) {
      return new IntervalNode(range);
    }
    if (range.start < node.range.start) {
      node.left = this.insertNode(node.left, range);
    } else {
      node.right = this.insertNode(node.right, range);
    }
    node.max = this.bigintMax(node.max, range.end);
    return node;
  }
  bigintMax(...args) {
    return args.reduce((max, current) => {
      return current > max ? current : max;
    }, args[0]);
  }
  findOverlappingRanges(node, range, result) {
    if (!node) {
      return;
    }
    if (this.doRangesOverlap(node.range, range)) {
      result.push(node.range);
    }
    if (node.left && node.left.max >= range.start) {
      this.findOverlappingRanges(node.left, range, result);
    }
    if (node.right && node.range.start <= range.end) {
      this.findOverlappingRanges(node.right, range, result);
    }
  }
  doRangesOverlap(a, b) {
    return a.start < b.end && b.start < a.end;
  }
  removeNode(node, range) {
    if (!node) {
      return null;
    }
    if (this.areRangesEqual(node.range, range)) {
      if (!node.left) {
        return node.right;
      }
      if (!node.right) {
        return node.left;
      }
      const successor = this.findMin(node.right);
      node.range = successor.range;
      node.right = this.removeNode(node.right, successor.range);
    } else if (range.start < node.range.start) {
      node.left = this.removeNode(node.left, range);
    } else {
      node.right = this.removeNode(node.right, range);
    }
    node.max = node.range.end;
    if (node.left) {
      node.max = this.bigintMax(node.max, node.left.max);
    }
    if (node.right) {
      node.max = this.bigintMax(node.max, node.right.max);
    }
    return node;
  }
  findMin(node) {
    let current = node;
    while (current.left) {
      current = current.left;
    }
    return current;
  }
  areRangesEqual(a, b) {
    return a.start === b.start && a.end === b.end && a.pid === b.pid;
  }
  findLocksForProcessInNode(node, pid, result) {
    if (!node) {
      return;
    }
    if (node.range.pid === pid) {
      result.push(node.range);
    }
    this.findLocksForProcessInNode(node.left, pid, result);
    this.findLocksForProcessInNode(node.right, pid, result);
  }
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  FileLock,
  FileLockManagerForNode,
  createNodeFsMountHandler,
  getPHPLoaderModule,
  loadNodeRuntime,
  useHostFilesystem,
  withNetworking,
  withXdebug
});
