diff --git a/package-lock.json b/package-lock.json index 2bfcfb768d0..368f1ef441b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12837,6 +12837,10 @@ "resolved": "packages/php-wasm/logger", "link": true }, + "node_modules/@php-wasm/mariadb-wasm-compile": { + "resolved": "packages/php-wasm/mariadb-wasm-compile", + "link": true + }, "node_modules/@php-wasm/node": { "resolved": "packages/php-wasm/node", "link": true @@ -20404,6 +20408,10 @@ "resolved": "packages/playground/devtools-extension", "link": true }, + "node_modules/@wp-playground/mariadb": { + "resolved": "packages/playground/mariadb", + "link": true + }, "node_modules/@wp-playground/mcp": { "resolved": "packages/playground/mcp", "link": true @@ -53473,6 +53481,11 @@ "npm": ">=10.2.3" } }, + "packages/php-wasm/mariadb-wasm-compile": { + "name": "@php-wasm/mariadb-wasm-compile", + "version": "0.1.0", + "license": "GPL-2.0-or-later" + }, "packages/php-wasm/node": { "name": "@php-wasm/node", "version": "3.1.18", @@ -53802,6 +53815,15 @@ "node": ">=0.10.0" } }, + "packages/playground/mariadb": { + "name": "@wp-playground/mariadb", + "version": "3.1.18", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, "packages/playground/mcp": { "name": "@wp-playground/mcp", "version": "3.1.18", diff --git a/packages/php-wasm/mariadb-wasm-compile/.gitignore b/packages/php-wasm/mariadb-wasm-compile/.gitignore new file mode 100644 index 00000000000..148fcf36041 --- /dev/null +++ b/packages/php-wasm/mariadb-wasm-compile/.gitignore @@ -0,0 +1,6 @@ +# MariaDB source tree (cloned on demand by build.sh) +mariadb/ + +# Build artifacts (intermediate — final output is in dist/) +build-host/ +build-wasm/ diff --git a/packages/php-wasm/mariadb-wasm-compile/LICENSE b/packages/php-wasm/mariadb-wasm-compile/LICENSE new file mode 100644 index 00000000000..6eb160c018c --- /dev/null +++ b/packages/php-wasm/mariadb-wasm-compile/LICENSE @@ -0,0 +1,26 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Note: This license applies only to the build scripts, wrapper code, and +documentation in this repository. MariaDB itself is licensed under GPL v2. +See mariadb/COPYING for MariaDB's license terms. Any binary distribution of +the compiled WASM output must comply with the GPL. diff --git a/packages/php-wasm/mariadb-wasm-compile/README.md b/packages/php-wasm/mariadb-wasm-compile/README.md new file mode 100644 index 00000000000..66d432f1999 --- /dev/null +++ b/packages/php-wasm/mariadb-wasm-compile/README.md @@ -0,0 +1,201 @@ +# MariaDB → WebAssembly + +Compile MariaDB's embedded server (`libmysqld`) to WebAssembly using Emscripten. This gives you a full MariaDB SQL engine running in-process — no TCP server, no daemon, just direct C API calls from JavaScript. + +This is an experimental project. MariaDB was never designed for WASM, and there are real limitations (see [Shortcomings](#shortcomings) below). But the embedded server architecture makes it the most viable path: it bundles the SQL parser, optimizer, and storage engines into a single library that talks through function calls instead of sockets. + +## Why MariaDB and not MySQL? + +MySQL 8.0 removed the embedded server (`libmysqld`). MariaDB still maintains it. That's the entire reason — without an embedded server mode, you'd need to emulate a full TCP daemon inside WASM, which is dramatically harder. + +## Prerequisites + +**Emscripten** (the C/C++ → WASM compiler): + +```bash +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +./emsdk install latest +./emsdk activate latest +source ./emsdk_env.sh +``` + +**Build tools**: `cmake`, `make`, a native C/C++ compiler (gcc or clang). These are needed for the host build stage. + +On Ubuntu/Debian: + +```bash +sudo apt install build-essential cmake bison libncurses-dev +``` + +On macOS: + +```bash +brew install cmake bison +``` + +## Building + +Clone with submodules: + +```bash +git clone --recursive https://github.com/user/try-mysql-wasm.git +cd try-mysql-wasm +``` + +If you already cloned without `--recursive`: + +```bash +git submodule update --init +``` + +Run the build: + +```bash +./build.sh +``` + +This runs three stages: + +1. **Host build** — Compiles MariaDB's code-generation tools (`comp_err`, `comp_sql`, `gen_lex_hash`, `gen_lex_token`, `factorial`, `uca-dump`) natively. These tools run during the build to generate source files (error message tables, SQL keyword hashes, etc.) and can't run as WASM. + +2. **WASM cross-compile** — Runs `emcmake cmake` + `emmake make` to cross-compile MariaDB with Emscripten, using the host-built tools from stage 1 via `IMPORT_EXECUTABLES`. + +3. **Link** — Links all the static libraries into a final `dist/mariadb.wasm` + `dist/mariadb.js` module. + +You can run stages individually: + +```bash +./build.sh host # Stage 1 only +./build.sh wasm # Stage 2 only +./build.sh link # Stage 3 only +./build.sh clean # Remove all build artifacts +``` + +Control parallelism with `JOBS`: + +```bash +JOBS=8 ./build.sh +``` + +## Usage + +The output is an Emscripten module that exposes the MariaDB C API (`mysql_init`, `mysql_query`, `mysql_store_result`, etc.) to JavaScript. + +```javascript +import createMariaDB from './dist/mariadb.js'; + +const db = await createMariaDB(); + +const mysql_server_init = db.cwrap('mysql_server_init', 'number', ['number', 'number', 'number']); +const mysql_init = db.cwrap('mysql_init', 'number', ['number']); +const mysql_real_connect = db.cwrap('mysql_real_connect', 'number', ['number', 'string', 'string', 'string', 'string', 'number', 'string', 'number']); +const mysql_query = db.cwrap('mysql_query', 'number', ['number', 'string']); +const mysql_store_result = db.cwrap('mysql_store_result', 'number', ['number']); +const mysql_close = db.cwrap('mysql_close', null, ['number']); +const mysql_server_end = db.cwrap('mysql_server_end', null, []); + +mysql_server_init(0, 0, 0); +const conn = mysql_init(0); +mysql_real_connect(conn, null, null, null, null, 0, null, 0); + +mysql_query(conn, 'CREATE TABLE t (id INT, name VARCHAR(50)) ENGINE=MEMORY'); +mysql_query(conn, "INSERT INTO t VALUES (1, 'hello')"); + +const result = mysql_store_result(conn); +// ... fetch rows ... + +mysql_close(conn); +mysql_server_end(); +``` + +See `example/demo.mjs` for a complete working example. + +## What's included + +The WASM build includes these storage engines: + +| Engine | Type | Notes | +| ----------------- | ---------- | --------------------------------------------------------------------------------------------------------- | +| **MEMORY (HEAP)** | In-memory | Best fit for WASM. All data lives in WASM linear memory. No persistence. | +| **MyISAM** | File-based | Works through Emscripten's virtual filesystem (MEMFS). Data is volatile unless you use NODEFS in Node.js. | +| **Aria** | File-based | Crash-safe MyISAM replacement. Same MEMFS caveats as MyISAM. | +| **CSV** | File-based | Reads/writes CSV files through the virtual filesystem. | +| **ARCHIVE** | File-based | Compressed read-heavy storage. | +| **BLACKHOLE** | /dev/null | Accepts writes, stores nothing. Useful for testing SQL syntax. | +| **SEQUENCE** | Virtual | Generates number sequences. No storage needed. | + +## Shortcomings + +This is not a production database. It's an experiment in pushing a large C/C++ server codebase into WASM. Here's what doesn't work or works poorly. + +### No InnoDB + +InnoDB is disabled entirely. It depends on: + +- **Asynchronous I/O** (`libaio` or `io_uring`) — these are Linux kernel interfaces with no WASM equivalent. +- **Complex threading** — InnoDB runs background threads for page cleaning, log writing, purging old row versions, and buffer pool management. Emscripten's pthread support exists but has sharp edges (async thread creation, no blocking on the main thread, no POSIX signals). +- **Durable fsync semantics** — InnoDB's crash recovery assumes `fsync()` actually flushes to stable storage. In Emscripten's MEMFS, `fsync()` is a no-op. Everything is volatile. +- **Doublewrite buffer** — assumes specific filesystem behavior that can't be guaranteed in a virtual FS. + +This means no transactions, no ACID guarantees, no foreign keys (with enforcement), no row-level locking. You get MyISAM-level functionality: table-level locking, no crash recovery, no rollback. + +### No persistence (by default) + +With MEMFS (the default), all data disappears when the WASM module is unloaded. You can use NODEFS in Node.js to map directories to real files, or IndexedDB-backed IDBFS in browsers, but neither gives you the durability guarantees a real database expects. + +### No networking + +The embedded server has no TCP listener. You can't connect to it with `mysql` CLI or any standard database driver. All access is through the in-process C API, which you call via Emscripten's `ccall`/`cwrap`. + +### No multi-client concurrency + +The embedded server runs single-threaded from the caller's perspective. There's one connection handle, one query at a time. You can create multiple `MYSQL*` handles, but they share the same server state and aren't designed for concurrent access from multiple threads. + +### Large binary size + +MariaDB is a large codebase. Even with most plugins stripped, expect the `.wasm` file to be 10-30 MB (before gzip). For comparison, sql.js (SQLite compiled to WASM) is ~1 MB. This makes it impractical for casual browser use where download size matters. + +### No signals, no fork + +Emscripten doesn't support POSIX signals or `fork()`. MariaDB uses signals internally for shutdown coordination and alarm handling. The embedded server path avoids most of this, but some codepaths may hit stub implementations that silently do nothing. + +### No prepared statements (possibly) + +The embedded server supports prepared statements in theory, but the WASM calling conventions for the binary protocol (which uses `mysql_stmt_*` functions with pointer-heavy structs) may be fragile. Text-mode queries via `mysql_query()` are the safer path. + +### No authentication + +The embedded server bypasses authentication entirely — you connect as root with no password. This is by design (you're connecting to yourself), but it means you can't test auth plugins or user permission workflows. + +### No character set auto-detection + +Character set initialization reads system locale settings that don't exist in WASM. The build hardcodes UTF-8 defaults, but edge cases around `SET NAMES`, collation detection, or locale-dependent sorting may behave differently than on a real OS. + +### Memory pressure + +MariaDB's memory allocator and the SQL optimizer's memory usage patterns were designed for machines with gigabytes of RAM. In WASM, you're constrained by the browser's memory limits (typically 2-4 GB for the entire tab). Complex queries, large temporary tables, or big sort buffers can hit memory limits faster than you'd expect. + +### Build is fragile + +The Emscripten cross-compilation hits edge cases in MariaDB's build system: + +- CMake feature-detection tests (`TRY_RUN`) fail during cross-compilation because the test programs can't execute. The build script hardcodes results for known checks (`STACK_DIRECTION`, `HAVE_IB_GCC_ATOMIC_BUILTINS`) but there may be others. +- Some system headers expected by MariaDB don't exist in Emscripten's sysroot. The build may need patches as MariaDB or Emscripten evolve. +- Upgrading either MariaDB or Emscripten versions may break the build in ways that require manual investigation. + +## How this compares to alternatives + +| Approach | Database | Binary size | Performance | Fidelity | +| ---------------- | ----------------- | ----------- | --------------- | ----------------------- | +| **sql.js** | SQLite → WASM | ~1 MB | Fast | Full SQLite | +| **PGlite** | PostgreSQL → WASM | ~3 MB gzip | Good | High (single-user mode) | +| **DuckDB-WASM** | DuckDB → WASM | ~5 MB | Fast | Full DuckDB | +| **mysql-wasm** | MySQL in x86 VM | ~50 MB | Slow (emulated) | Full MySQL | +| **This project** | MariaDB → WASM | ~10-30 MB | Moderate | Partial (no InnoDB) | + +If you need an SQL database in the browser and don't specifically need MySQL/MariaDB compatibility, **sql.js** or **PGlite** are more mature choices. This project is useful if you need to test MariaDB-specific SQL syntax, MyISAM behavior, or MariaDB's optimizer in an isolated environment. + +## License + +The build scripts and wrapper code in this repository are MIT licensed. MariaDB itself is GPL v2 — see `mariadb/COPYING` for details. Any binary distribution of the WASM output must comply with the GPL. diff --git a/packages/php-wasm/mariadb-wasm-compile/build.sh b/packages/php-wasm/mariadb-wasm-compile/build.sh new file mode 100755 index 00000000000..d017043c137 --- /dev/null +++ b/packages/php-wasm/mariadb-wasm-compile/build.sh @@ -0,0 +1,460 @@ +#!/usr/bin/env bash +# +# Build MariaDB's embedded server (libmysqld) as WebAssembly using Emscripten. +# +# This is a two-stage cross-compilation process: +# Stage 1: Build helper executables natively (comp_err, comp_sql, etc.) +# These tools generate source files at build time and must run on +# the host machine, not under WASM. +# Stage 2: Cross-compile MariaDB for WASM using Emscripten, pointing +# IMPORT_EXECUTABLES at the Stage 1 output so CMake can find +# the host-built generators. +# +# The build targets libmysqld (the embedded server library), which provides +# the full MariaDB SQL engine as an in-process library — no TCP sockets, no +# daemon process, just direct C API calls. This is the only viable path to +# running MariaDB in a WASM sandbox. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MARIADB_SRC="$SCRIPT_DIR/mariadb" +HOST_BUILD_DIR="$SCRIPT_DIR/build-host" +WASM_BUILD_DIR="$SCRIPT_DIR/build-wasm" +OUTPUT_DIR="$SCRIPT_DIR/dist" + +PARALLEL_JOBS="${JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}" + +# --------------------------------------------------------------------------- +# Preflight checks +# --------------------------------------------------------------------------- + +MARIADB_BRANCH="11.4" + +if [ ! -f "$MARIADB_SRC/CMakeLists.txt" ]; then + echo "MariaDB source not found. Cloning branch $MARIADB_BRANCH..." + git clone --depth 1 --branch "$MARIADB_BRANCH" --recurse-submodules \ + https://github.com/MariaDB/server.git "$MARIADB_SRC" +fi + +if ! command -v emcmake &>/dev/null; then + echo "Error: Emscripten not found. Install it and run 'source emsdk_env.sh'" + echo "See: https://emscripten.org/docs/getting_started/downloads.html" + exit 1 +fi + +if ! command -v cmake &>/dev/null; then + echo "Error: cmake not found." + exit 1 +fi + +echo "=== MariaDB WASM Build ===" +echo "Source: $MARIADB_SRC" +echo "Host dir: $HOST_BUILD_DIR" +echo "WASM dir: $WASM_BUILD_DIR" +echo "Output: $OUTPUT_DIR" +echo "Jobs: $PARALLEL_JOBS" +echo "" + +# --------------------------------------------------------------------------- +# Stage 1: Native host build — only the helper executables +# --------------------------------------------------------------------------- +# MariaDB's build generates source files using small C/C++ tools like +# comp_err (compiles error message files), comp_sql (compiles SQL scripts +# into C arrays), gen_lex_hash and gen_lex_token (generate the SQL lexer +# hash tables), factorial, and uca-dump. When cross-compiling, these tools +# can't run under the target (WASM), so we build them natively first and +# then tell the cross-compile stage where to find them via +# IMPORT_EXECUTABLES. +# --------------------------------------------------------------------------- + +build_host() { + echo ">>> Stage 1: Building host helper executables..." + mkdir -p "$HOST_BUILD_DIR" + cd "$HOST_BUILD_DIR" + + cmake "$MARIADB_SRC" \ + -DCMAKE_BUILD_TYPE=Release \ + -DWITHOUT_SERVER=OFF \ + -DWITH_UNIT_TESTS=OFF \ + -DPLUGIN_INNODB=NO \ + -DPLUGIN_MROONGA=NO \ + -DPLUGIN_TOKUDB=NO \ + -DPLUGIN_ROCKSDB=NO \ + -DPLUGIN_SPIDER=NO \ + -DPLUGIN_SPHINX=NO \ + -DPLUGIN_CONNECT=NO \ + -DPLUGIN_PERFSCHEMA=NO \ + -DPLUGIN_COLUMNSTORE=NO \ + -DPLUGIN_OQGRAPH=NO \ + -DPLUGIN_FEDERATED=NO \ + -DPLUGIN_FEDERATEDX=NO \ + 2>&1 | tail -20 + + make -j"$PARALLEL_JOBS" import_executables 2>&1 | tail -10 + + if [ ! -f "$HOST_BUILD_DIR/import_executables.cmake" ]; then + echo "Error: import_executables.cmake was not generated" + exit 1 + fi + + echo ">>> Stage 1 complete. Host executables ready." + echo "" +} + +# --------------------------------------------------------------------------- +# Stage 2: WASM cross-compilation with Emscripten +# --------------------------------------------------------------------------- +# We build the embedded server (libmysqld) which bundles the SQL parser, +# optimizer, and storage engines into a single library. We disable every +# feature that depends on OS primitives that don't exist in WASM: +# - No dynamic plugins (no dlopen) +# - No InnoDB (needs libaio/liburing, complex threading, fsync semantics) +# - No networking plugins +# - No performance schema (OS instrumentation) +# - No auth_gssapi (Kerberos) +# - No backup tools +# +# We keep MyISAM and HEAP (MEMORY) storage engines. HEAP is the best fit +# for WASM since it's purely in-memory. MyISAM is file-based but works +# with Emscripten's virtual filesystem (MEMFS). +# --------------------------------------------------------------------------- + +build_wasm() { + echo ">>> Stage 2: Cross-compiling MariaDB for WASM..." + mkdir -p "$WASM_BUILD_DIR" + cd "$WASM_BUILD_DIR" + + # Patch readline.cmake to skip the curses dependency when cross- + # compiling for Emscripten. The embedded server has no interactive + # terminal, so readline/curses are never used. + sed -i.bak 's/FIND_CURSES()/# FIND_CURSES() — skipped for Emscripten/' \ + "$MARIADB_SRC/cmake/readline.cmake" + + # Skip the MariaDB Connector/C build entirely. The embedded server + # is accessed via direct C API calls (cwrap), not through a client + # library. The connector needs GnuTLS/OpenSSL which don't exist in + # the Emscripten sysroot. + cat > "$MARIADB_SRC/cmake/mariadb_connector_c.cmake" << 'PATCH' +# Stubbed out for Emscripten — we only need the embedded server, +# not the client connector library. +MESSAGE("== Skipping MariaDB Connector/C (not needed for embedded server)") +SET(MARIADB_CONNECTOR_C_VERSION "stub") +PATCH + + # Patch thr_timer.c to skip thread creation. The timer thread + # manages query timeouts via setitimer/threads — neither works in + # WASM. For the embedded server this is fine: it's single-threaded + # and doesn't need timeouts. + python3 -c " +import sys +path = sys.argv[1] +txt = open(path).read() +# Replace the thread creation block with a simple success return. +# We keep the queue/mutex init but skip the timer_handler thread. +txt = txt.replace( + '/* Create a thread to handle timers */', + '/* Create a thread to handle timers */\n' + '#ifdef __EMSCRIPTEN__\n' + ' /* Skip timer thread in WASM — no threading, no timeouts needed */\n' + ' DBUG_RETURN(0);\n' + '#endif' +) +open(path, 'w').write(txt) +" "$MARIADB_SRC/mysys/thr_timer.c" + + # Patch mysqld.cc so --skip-grant-tables also skips loading + # the mysql.servers table. Without this, the embedded server + # errors on "Can't open and lock privilege tables" even with + # --skip-grant-tables because servers_init() is hardcoded to + # always read the table. + sed -i.bak 's/servers_init(0)/servers_init(opt_noacl)/' \ + "$MARIADB_SRC/sql/mysqld.cc" + + + # Patch Aria for WASM: skip control file, translog, recovery, + # and checkpoint threads, but keep maria_init() and pagecache + # so Aria can handle temp tables. + python3 -c " +import sys +path = sys.argv[1] +txt = open(path).read() +# Insert an #ifdef block that does the minimal init needed for +# temp tables: maria_init + pagecache, but no control file/translog +txt = txt.replace( + 'static int ha_maria_init(void *p)\n{', + 'static int ha_maria_init(void *p)\n{\n' + '#ifdef __EMSCRIPTEN__\n' + ' { int res= 0;\n' + ' maria_hton= (handlerton *)p;\n' + ' maria_hton->db_type= DB_TYPE_ARIA;\n' + ' maria_hton->create= maria_create_handler;\n' + ' maria_hton->panic= maria_hton_panic;\n' + ' maria_hton->tablefile_extensions= ha_maria_exts;\n' + ' maria_hton->commit= maria_commit;\n' + ' maria_hton->rollback= maria_rollback;\n' + ' maria_hton->flags= (HTON_CAN_RECREATE | HTON_SUPPORT_LOG_TABLES |\n' + ' HTON_NO_ROLLBACK |\n' + ' HTON_TRANSACTIONAL_AND_NON_TRANSACTIONAL);\n' + ' bzero(maria_log_pagecache, sizeof(*maria_log_pagecache));\n' + ' maria_tmpdir= &mysql_tmpdir_list;\n' + ' res= maria_init();\n' + ' if (!res) {\n' + ' res= !init_pagecache(maria_pagecache,\n' + ' (size_t) pagecache_buffer_size, pagecache_division_limit,\n' + ' pagecache_age_threshold, maria_block_size, pagecache_file_hash_size,\n' + ' 0);\n' + ' }\n' + ' if (!res) {\n' + ' res= !init_pagecache(maria_log_pagecache,\n' + ' TRANSLOG_PAGECACHE_SIZE, 0, 0,\n' + ' TRANSLOG_PAGE_SIZE, 0, 0);\n' + ' }\n' + + ' if (res)\n' + ' maria_hton= 0;\n' + ' maria_multi_threaded= maria_in_ha_maria= TRUE;\n' + ' maria_create_trn_hook= maria_create_trn_for_mysql;\n' + ' maria_pagecache->extra_debug= 1;\n' + ' return res; }\n' + '#endif' +) +open(path, 'w').write(txt) +" "$MARIADB_SRC/storage/maria/ha_maria.cc" + + # Patch pcre.cmake for Emscripten cross-compilation: + # 1. Pass the toolchain file so cmake knows we're targeting WASM + # 2. Strip macOS-specific -arch/-isysroot flags from C flags that + # get inherited from the parent cmake and break emcc + python3 -c " +import sys +path = sys.argv[1] +txt = open(path).read() +# Add toolchain file to ExternalProject cmake args +txt = txt.replace( + '\"-DCMAKE_C_COMPILER=\${CMAKE_C_COMPILER}\"', + '\"-DCMAKE_C_COMPILER=\${CMAKE_C_COMPILER}\"\n' + ' \"-DCMAKE_TOOLCHAIN_FILE=\${CMAKE_TOOLCHAIN_FILE}\"' +) +# Strip macOS flags from pcre2_flags before they're passed +txt = txt.replace( + 'SET(pcre2_flags\${v} \"\${CMAKE_C_FLAGS\${v}}\")', + 'STRING(REGEX REPLACE \"-arch [^ ]+\" \"\" _clean_flags\${v} \"\${CMAKE_C_FLAGS\${v}}\")\n' + ' STRING(REGEX REPLACE \"-isysroot [^ ]+\" \"\" _clean_flags\${v} \"\${_clean_flags\${v}}\")\n' + ' SET(pcre2_flags\${v} \"\${_clean_flags\${v}}\")' +) +open(path, 'w').write(txt) +" "$MARIADB_SRC/cmake/pcre.cmake" + + # Also skip building client tools, tests, and the minbuild/smoketest + # targets which depend on the connector library we just stubbed out. + python3 -c " +import re, sys +path = sys.argv[1] +txt = open(path).read() +txt = txt.replace('ADD_SUBDIRECTORY(client)', '# ADD_SUBDIRECTORY(client)') +txt = txt.replace('ADD_SUBDIRECTORY(tests)', '# ADD_SUBDIRECTORY(tests)') +# Comment out the entire IF(NOT WITHOUT_SERVER) block at the end that +# defines minbuild and smoketest targets (they need client binaries). +# Remove the final IF(NOT WITHOUT_SERVER) block that defines minbuild +# and smoketest targets. We find the last occurrence and remove +# everything from it to its matching ENDIF() (handling nesting). +idx = txt.rfind('IF(NOT WITHOUT_SERVER)') +if idx >= 0: + depth = 0 + i = idx + end_idx = len(txt) + while i < len(txt): + line = '' + j = txt.find('\n', i) + if j < 0: j = len(txt) + line = txt[i:j].strip().upper() + if line.startswith('IF(') or line.startswith('IF ('): + depth += 1 + elif line.startswith('ENDIF'): + depth -= 1 + if depth == 0: + end_idx = j + 1 + break + i = j + 1 + txt = txt[:idx] + '# minbuild/smoketest targets skipped for Emscripten\n' + txt[end_idx:] +open(path, 'w').write(txt) +" "$MARIADB_SRC/CMakeLists.txt" + + emcmake cmake "$MARIADB_SRC" \ + -DCMAKE_BUILD_TYPE=Release \ + -DIMPORT_EXECUTABLES="$HOST_BUILD_DIR/import_executables.cmake" \ + \ + -DSTACK_DIRECTION=-1 \ + -DHAVE_IB_GCC_ATOMIC_BUILTINS=1 \ + \ + -DWITH_EMBEDDED_SERVER=ON \ + -DWITHOUT_SERVER=OFF \ + -DWITHOUT_DYNAMIC_PLUGINS=1 \ + -DDISABLE_SHARED=1 \ + -DENABLED_PROFILING=OFF \ + -DENABLE_DTRACE=OFF \ + -DWITH_SAFEMALLOC=OFF \ + \ + -DWITH_SSL=bundled \ + -DWITH_ZLIB=bundled \ + \ + -DWITH_UNIT_TESTS=OFF \ + -DWITH_MARIABACKUP=OFF \ + -DWITH_WSREP=OFF \ + \ + -DPLUGIN_ARCHIVE=STATIC \ + -DPLUGIN_BLACKHOLE=STATIC \ + -DPLUGIN_CSV=STATIC \ + -DPLUGIN_HEAP=STATIC \ + -DPLUGIN_MYISAM=STATIC \ + -DPLUGIN_MYISAMMRG=STATIC \ + -DPLUGIN_SEQUENCE=STATIC \ + \ + -DPLUGIN_INNODB=NO \ + -DPLUGIN_INNOBASE=NO \ + -DPLUGIN_MROONGA=NO \ + -DPLUGIN_TOKUDB=NO \ + -DPLUGIN_ROCKSDB=NO \ + -DPLUGIN_SPIDER=NO \ + -DPLUGIN_SPHINX=NO \ + -DPLUGIN_CONNECT=NO \ + -DPLUGIN_PERFSCHEMA=NO \ + -DPLUGIN_COLUMNSTORE=NO \ + -DPLUGIN_OQGRAPH=NO \ + -DPLUGIN_FEDERATED=NO \ + -DPLUGIN_FEDERATEDX=NO \ + -DPLUGIN_AUTH_GSSAPI=NO \ + -DPLUGIN_AUTH_PAM=NO \ + -DPLUGIN_MARIA=NO \ + \ + -DCMAKE_C_FLAGS="-O2 -DHAVE_DLERROR -Wno-implicit-function-declaration -I$WASM_BUILD_DIR/extra/pcre2/src/pcre2-build/interface" \ + -DCMAKE_CXX_FLAGS="-O2 -DHAVE_DLERROR -I$WASM_BUILD_DIR/extra/pcre2/src/pcre2-build/interface" \ + -DCMAKE_EXE_LINKER_FLAGS="-sNODERAWFS=1" \ + 2>&1 | tail -30 + + # Build the embedded server library + emmake make -j"$PARALLEL_JOBS" mysqlserver 2>&1 | tail -20 + + echo ">>> Stage 2 complete." + echo "" +} + +# --------------------------------------------------------------------------- +# Stage 3: Link the WASM module +# --------------------------------------------------------------------------- +# Take the static libraries produced by Stage 2 and link them into a single +# .wasm + .js file using emcc. We expose the mysql_* C API functions so +# JavaScript can call them. The module uses MEMFS (Emscripten's in-memory +# filesystem) to store any data files that MyISAM or the server internals +# create at runtime. +# --------------------------------------------------------------------------- + +link_wasm() { + echo ">>> Stage 3: Linking WASM module..." + mkdir -p "$OUTPUT_DIR" + + # Find the embedded server archive + LIBMYSQLD=$(find "$WASM_BUILD_DIR" -name "libmysqld.a" -o -name "libmariadbd.a" | head -1) + if [ -z "$LIBMYSQLD" ]; then + # Try the combined server library + LIBMYSQLD=$(find "$WASM_BUILD_DIR" -name "libmysqlserver.a" -o -name "libmariadbserver.a" | head -1) + fi + + if [ -z "$LIBMYSQLD" ]; then + echo "Error: Could not find embedded server library" + echo "Available .a files:" + find "$WASM_BUILD_DIR" -name "*.a" | head -20 + exit 1 + fi + + echo "Using library: $LIBMYSQLD" + + # Collect all static libraries that make up the embedded server + LIBS=$(find "$WASM_BUILD_DIR" -name "*.a" | grep -v CMakeFiles | sort) + + emcc \ + -O2 \ + -sALLOW_MEMORY_GROWTH=1 \ + -sINITIAL_MEMORY=67108864 \ + -sSTACK_SIZE=1048576 \ + -sMODULARIZE=1 \ + -sEXPORT_NAME=createMariaDB \ + -sEXPORTED_FUNCTIONS='[ + "_mysql_server_init", + "_mysql_server_end", + "_mysql_init", + "_mysql_real_connect", + "_mysql_close", + "_mysql_query", + "_mysql_store_result", + "_mysql_fetch_row", + "_mysql_num_fields", + "_mysql_num_rows", + "_mysql_free_result", + "_mysql_error", + "_mysql_errno", + "_mysql_affected_rows", + "_mysql_field_count", + "_mysql_fetch_field", + "_mysql_fetch_lengths", + "_mysql_real_escape_string", + "_mysql_select_db", + "_mysql_info", + "_mysql_get_server_info", + "_mysql_insert_id", + "_malloc", + "_free" + ]' \ + -sEXPORTED_RUNTIME_METHODS='["ccall","cwrap","UTF8ToString","stringToUTF8","allocateUTF8","getValue","FS","NODEFS"]' \ + -lnodefs.js \ + -sERROR_ON_UNDEFINED_SYMBOLS=0 \ + -sFILESYSTEM=1 \ + -sEXIT_RUNTIME=0 \ + -sWASM_BIGINT=1 \ + -sENVIRONMENT='web,node' \ + --no-entry \ + $LIBS \ + -o "$OUTPUT_DIR/mariadb.js" + + echo "" + echo ">>> Build complete!" + echo "" + ls -lh "$OUTPUT_DIR"/mariadb.* +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +case "${1:-all}" in + host) + build_host + ;; + wasm) + build_wasm + ;; + link) + link_wasm + ;; + all) + build_host + build_wasm + link_wasm + ;; + clean) + echo "Removing build directories..." + rm -rf "$HOST_BUILD_DIR" "$WASM_BUILD_DIR" "$OUTPUT_DIR" + echo "Clean." + ;; + *) + echo "Usage: $0 [host|wasm|link|all|clean]" + echo "" + echo " host — Build native helper executables only" + echo " wasm — Cross-compile MariaDB to WASM only" + echo " link — Link the final .wasm module only" + echo " all — Run all stages (default)" + echo " clean — Remove all build artifacts" + exit 1 + ;; +esac diff --git a/packages/php-wasm/mariadb-wasm-compile/dist/mariadb.js b/packages/php-wasm/mariadb-wasm-compile/dist/mariadb.js new file mode 100644 index 00000000000..b5f6271f7ef --- /dev/null +++ b/packages/php-wasm/mariadb-wasm-compile/dist/mariadb.js @@ -0,0 +1,21 @@ +var createMariaDB = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename; + return ( +async function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";if(ENVIRONMENT_IS_NODE){}var moduleOverrides={...Module};var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);Module["HEAP64"]=HEAP64=new BigInt64Array(b);Module["HEAPU64"]=HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();SOCKFS.root=FS.mount(SOCKFS,{},null);wasmExports["__wasm_call_ctors"]();FS.ignorePermissions=false}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("mariadb.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{receiveInstance(mod,inst);resolve(mod.exports)})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.unshift(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.unshift(cb);function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var ERRNO_CODES={EPERM:63,ENOENT:44,ESRCH:71,EINTR:27,EIO:29,ENXIO:60,E2BIG:1,ENOEXEC:45,EBADF:8,ECHILD:12,EAGAIN:6,EWOULDBLOCK:6,ENOMEM:48,EACCES:2,EFAULT:21,ENOTBLK:105,EBUSY:10,EEXIST:20,EXDEV:75,ENODEV:43,ENOTDIR:54,EISDIR:31,EINVAL:28,ENFILE:41,EMFILE:33,ENOTTY:59,ETXTBSY:74,EFBIG:22,ENOSPC:51,ESPIPE:70,EROFS:69,EMLINK:34,EPIPE:64,EDOM:18,ERANGE:68,ENOMSG:49,EIDRM:24,ECHRNG:106,EL2NSYNC:156,EL3HLT:107,EL3RST:108,ELNRNG:109,EUNATCH:110,ENOCSI:111,EL2HLT:112,EDEADLK:16,ENOLCK:46,EBADE:113,EBADR:114,EXFULL:115,ENOANO:104,EBADRQC:103,EBADSLT:102,EDEADLOCK:16,EBFONT:101,ENOSTR:100,ENODATA:116,ETIME:117,ENOSR:118,ENONET:119,ENOPKG:120,EREMOTE:121,ENOLINK:47,EADV:122,ESRMNT:123,ECOMM:124,EPROTO:65,EMULTIHOP:36,EDOTDOT:125,EBADMSG:9,ENOTUNIQ:126,EBADFD:127,EREMCHG:128,ELIBACC:129,ELIBBAD:130,ELIBSCN:131,ELIBMAX:132,ELIBEXEC:133,ENOSYS:52,ENOTEMPTY:55,ENAMETOOLONG:37,ELOOP:32,EOPNOTSUPP:138,EPFNOSUPPORT:139,ECONNRESET:15,ENOBUFS:42,EAFNOSUPPORT:5,EPROTOTYPE:67,ENOTSOCK:57,ENOPROTOOPT:50,ESHUTDOWN:140,ECONNREFUSED:14,EADDRINUSE:3,ECONNABORTED:13,ENETUNREACH:40,ENETDOWN:38,ETIMEDOUT:73,EHOSTDOWN:142,EHOSTUNREACH:23,EINPROGRESS:26,EALREADY:7,EDESTADDRREQ:17,EMSGSIZE:35,EPROTONOSUPPORT:66,ESOCKTNOSUPPORT:137,EADDRNOTAVAIL:4,ENETRESET:39,EISCONN:30,ENOTCONN:53,ETOOMANYREFS:141,EUSERS:136,EDQUOT:19,ESTALE:72,ENOTSUP:138,ENOMEDIUM:148,EILSEQ:25,EOVERFLOW:61,ECANCELED:11,ENOTRECOVERABLE:56,EOWNERDEAD:62,ESTRPIPE:135};var NODEFS={isWindows:false,staticInit(){NODEFS.isWindows=!!process.platform.match(/^win/);var flags=process.binding("constants")["fs"];NODEFS.flagsForNodeMap={1024:flags["O_APPEND"],64:flags["O_CREAT"],128:flags["O_EXCL"],256:flags["O_NOCTTY"],0:flags["O_RDONLY"],2:flags["O_RDWR"],4096:flags["O_SYNC"],512:flags["O_TRUNC"],1:flags["O_WRONLY"],131072:flags["O_NOFOLLOW"]}},convertNodeCode(e){var code=e.code;return ERRNO_CODES[code]},tryFSOperation(f){try{return f()}catch(e){if(!e.code)throw e;if(e.code==="UNKNOWN")throw new FS.ErrnoError(28);throw new FS.ErrnoError(NODEFS.convertNodeCode(e))}},mount(mount){return NODEFS.createNode(null,"/",NODEFS.getMode(mount.opts.root),0)},createNode(parent,name,mode,dev){if(!FS.isDir(mode)&&!FS.isFile(mode)&&!FS.isLink(mode)){throw new FS.ErrnoError(28)}var node=FS.createNode(parent,name,mode);node.node_ops=NODEFS.node_ops;node.stream_ops=NODEFS.stream_ops;return node},getMode(path){return NODEFS.tryFSOperation(()=>{var mode=fs.lstatSync(path).mode;if(NODEFS.isWindows){mode|=(mode&292)>>2}return mode})},realPath(node){var parts=[];while(node.parent!==node){parts.push(node.name);node=node.parent}parts.push(node.mount.opts.root);parts.reverse();return PATH.join(...parts)},flagsForNode(flags){flags&=~2097152;flags&=~2048;flags&=~32768;flags&=~524288;flags&=~65536;var newFlags=0;for(var k in NODEFS.flagsForNodeMap){if(flags&k){newFlags|=NODEFS.flagsForNodeMap[k];flags^=k}}if(flags){throw new FS.ErrnoError(28)}return newFlags},getattr(func,node){var stat=NODEFS.tryFSOperation(func);if(NODEFS.isWindows){if(!stat.blksize){stat.blksize=4096}if(!stat.blocks){stat.blocks=(stat.size+stat.blksize-1)/stat.blksize|0}stat.mode|=(stat.mode&292)>>2}return{dev:stat.dev,ino:node.id,mode:stat.mode,nlink:stat.nlink,uid:stat.uid,gid:stat.gid,rdev:stat.rdev,size:stat.size,atime:stat.atime,mtime:stat.mtime,ctime:stat.ctime,blksize:stat.blksize,blocks:stat.blocks}},setattr(arg,node,attr,chmod,utimes,truncate,stat){NODEFS.tryFSOperation(()=>{if(attr.mode!==undefined){var mode=attr.mode;if(NODEFS.isWindows){mode&=384}chmod(arg,mode);node.mode=attr.mode}if(typeof(attr.atime??attr.mtime)==="number"){var atime=new Date(attr.atime??stat(arg).atime);var mtime=new Date(attr.mtime??stat(arg).mtime);utimes(arg,atime,mtime)}if(attr.size!==undefined){truncate(arg,attr.size)}})},node_ops:{getattr(node){var path=NODEFS.realPath(node);return NODEFS.getattr(()=>fs.lstatSync(path),node)},setattr(node,attr){var path=NODEFS.realPath(node);if(attr.mode!=null&&attr.dontFollow){throw new FS.ErrnoError(52)}NODEFS.setattr(path,node,attr,fs.chmodSync,fs.utimesSync,fs.truncateSync,fs.lstatSync)},lookup(parent,name){var path=PATH.join2(NODEFS.realPath(parent),name);var mode=NODEFS.getMode(path);return NODEFS.createNode(parent,name,mode)},mknod(parent,name,mode,dev){var node=NODEFS.createNode(parent,name,mode,dev);var path=NODEFS.realPath(node);NODEFS.tryFSOperation(()=>{if(FS.isDir(node.mode)){fs.mkdirSync(path,node.mode)}else{fs.writeFileSync(path,"",{mode:node.mode})}});return node},rename(oldNode,newDir,newName){var oldPath=NODEFS.realPath(oldNode);var newPath=PATH.join2(NODEFS.realPath(newDir),newName);try{FS.unlink(newPath)}catch(e){}NODEFS.tryFSOperation(()=>fs.renameSync(oldPath,newPath));oldNode.name=newName},unlink(parent,name){var path=PATH.join2(NODEFS.realPath(parent),name);NODEFS.tryFSOperation(()=>fs.unlinkSync(path))},rmdir(parent,name){var path=PATH.join2(NODEFS.realPath(parent),name);NODEFS.tryFSOperation(()=>fs.rmdirSync(path))},readdir(node){var path=NODEFS.realPath(node);return NODEFS.tryFSOperation(()=>fs.readdirSync(path))},symlink(parent,newName,oldPath){var newPath=PATH.join2(NODEFS.realPath(parent),newName);NODEFS.tryFSOperation(()=>fs.symlinkSync(oldPath,newPath))},readlink(node){var path=NODEFS.realPath(node);return NODEFS.tryFSOperation(()=>fs.readlinkSync(path))},statfs(path){var stats=NODEFS.tryFSOperation(()=>fs.statfsSync(path));stats.frsize=stats.bsize;return stats}},stream_ops:{getattr(stream){return NODEFS.getattr(()=>fs.fstatSync(stream.nfd),stream.node)},setattr(stream,attr){NODEFS.setattr(stream.nfd,stream.node,attr,fs.fchmodSync,fs.futimesSync,fs.ftruncateSync,fs.fstatSync)},open(stream){var path=NODEFS.realPath(stream.node);NODEFS.tryFSOperation(()=>{stream.shared.refcount=1;stream.nfd=fs.openSync(path,NODEFS.flagsForNode(stream.flags))})},close(stream){NODEFS.tryFSOperation(()=>{if(stream.nfd&&--stream.shared.refcount===0){fs.closeSync(stream.nfd)}})},dup(stream){stream.shared.refcount++},read(stream,buffer,offset,length,position){return NODEFS.tryFSOperation(()=>fs.readSync(stream.nfd,new Int8Array(buffer.buffer,offset,length),0,length,position))},write(stream,buffer,offset,length,position){return NODEFS.tryFSOperation(()=>fs.writeSync(stream.nfd,new Int8Array(buffer.buffer,offset,length),0,length,position))},llseek(stream,offset,whence){var position=offset;if(whence===1){position+=stream.position}else if(whence===2){if(FS.isFile(stream.node.mode)){NODEFS.tryFSOperation(()=>{var stat=fs.fstatSync(stream.nfd);position+=stat.size})}}if(position<0){throw new FS.ErrnoError(28)}return position},mmap(stream,length,position,prot,flags){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}var ptr=mmapAlloc(length);NODEFS.stream_ops.read(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}},msync(stream,buffer,offset,length,mmapFlags){NODEFS.stream_ops.write(stream,buffer,0,length,offset,false);return 0}}};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS,NODEFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};var ___syscall__newselect=function(nfds,readfds,writefds,exceptfds,timeout){try{var total=0;var srcReadLow=readfds?HEAP32[readfds>>2]:0,srcReadHigh=readfds?HEAP32[readfds+4>>2]:0;var srcWriteLow=writefds?HEAP32[writefds>>2]:0,srcWriteHigh=writefds?HEAP32[writefds+4>>2]:0;var srcExceptLow=exceptfds?HEAP32[exceptfds>>2]:0,srcExceptHigh=exceptfds?HEAP32[exceptfds+4>>2]:0;var dstReadLow=0,dstReadHigh=0;var dstWriteLow=0,dstWriteHigh=0;var dstExceptLow=0,dstExceptHigh=0;var allLow=(readfds?HEAP32[readfds>>2]:0)|(writefds?HEAP32[writefds>>2]:0)|(exceptfds?HEAP32[exceptfds>>2]:0);var allHigh=(readfds?HEAP32[readfds+4>>2]:0)|(writefds?HEAP32[writefds+4>>2]:0)|(exceptfds?HEAP32[exceptfds+4>>2]:0);var check=(fd,low,high,val)=>fd<32?low&val:high&val;for(var fd=0;fd>2]:0,tv_usec=readfds?HEAP32[timeout+4>>2]:0;timeoutInMillis=(tv_sec+tv_usec/1e6)*1e3}flags=stream.stream_ops.poll(stream,timeoutInMillis)}if(flags&1&&check(fd,srcReadLow,srcReadHigh,mask)){fd<32?dstReadLow=dstReadLow|mask:dstReadHigh=dstReadHigh|mask;total++}if(flags&4&&check(fd,srcWriteLow,srcWriteHigh,mask)){fd<32?dstWriteLow=dstWriteLow|mask:dstWriteHigh=dstWriteHigh|mask;total++}if(flags&2&&check(fd,srcExceptLow,srcExceptHigh,mask)){fd<32?dstExceptLow=dstExceptLow|mask:dstExceptHigh=dstExceptHigh|mask;total++}}if(readfds){HEAP32[readfds>>2]=dstReadLow;HEAP32[readfds+4>>2]=dstReadHigh}if(writefds){HEAP32[writefds>>2]=dstWriteLow;HEAP32[writefds+4>>2]=dstWriteHigh}if(exceptfds){HEAP32[exceptfds>>2]=dstExceptLow;HEAP32[exceptfds+4>>2]=dstExceptHigh}return total}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}};function ___syscall_chmod(path,mode){try{path=SYSCALLS.getStr(path);FS.chmod(path,mode);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var SOCKFS={websocketArgs:{},callbacks:{},on(event,callback){SOCKFS.callbacks[event]=callback},emit(event,param){SOCKFS.callbacks[event]?.(param)},mount(mount){SOCKFS.websocketArgs=Module["websocket"]||{};(Module["websocket"]??={})["on"]=SOCKFS.on;return FS.createNode(null,"/",16895,0)},createSocket(family,type,protocol){type&=~526336;var streaming=type==1;if(streaming&&protocol&&protocol!=6){throw new FS.ErrnoError(66)}var sock={family,type,protocol,server:null,error:null,peers:{},pending:[],recv_queue:[],sock_ops:SOCKFS.websocket_sock_ops};var name=SOCKFS.nextname();var node=FS.createNode(SOCKFS.root,name,49152,0);node.sock=sock;var stream=FS.createStream({path:name,node,flags:2,seekable:false,stream_ops:SOCKFS.stream_ops});sock.stream=stream;return sock},getSocket(fd){var stream=FS.getStream(fd);if(!stream||!FS.isSocket(stream.node.mode)){return null}return stream.node.sock},stream_ops:{poll(stream){var sock=stream.node.sock;return sock.sock_ops.poll(sock)},ioctl(stream,request,varargs){var sock=stream.node.sock;return sock.sock_ops.ioctl(sock,request,varargs)},read(stream,buffer,offset,length,position){var sock=stream.node.sock;var msg=sock.sock_ops.recvmsg(sock,length);if(!msg){return 0}buffer.set(msg.buffer,offset);return msg.buffer.length},write(stream,buffer,offset,length,position){var sock=stream.node.sock;return sock.sock_ops.sendmsg(sock,buffer,offset,length)},close(stream){var sock=stream.node.sock;sock.sock_ops.close(sock)}},nextname(){if(!SOCKFS.nextname.current){SOCKFS.nextname.current=0}return`socket[${SOCKFS.nextname.current++}]`},websocket_sock_ops:{createPeer(sock,addr,port){var ws;if(typeof addr=="object"){ws=addr;addr=null;port=null}if(ws){if(ws._socket){addr=ws._socket.remoteAddress;port=ws._socket.remotePort}else{var result=/ws[s]?:\/\/([^:]+):(\d+)/.exec(ws.url);if(!result){throw new Error("WebSocket URL must be in the format ws(s)://address:port")}addr=result[1];port=parseInt(result[2],10)}}else{try{var url="ws://".replace("#","//");var subProtocols="binary";var opts=undefined;if(SOCKFS.websocketArgs["url"]){url=SOCKFS.websocketArgs["url"]}if(SOCKFS.websocketArgs["subprotocol"]){subProtocols=SOCKFS.websocketArgs["subprotocol"]}else if(SOCKFS.websocketArgs["subprotocol"]===null){subProtocols="null"}if(url==="ws://"||url==="wss://"){var parts=addr.split("/");url=url+parts[0]+":"+port+"/"+parts.slice(1).join("/")}if(subProtocols!=="null"){subProtocols=subProtocols.replace(/^ +| +$/g,"").split(/ *, */);opts=subProtocols}var WebSocketConstructor;if(ENVIRONMENT_IS_NODE){WebSocketConstructor=require("ws")}else{WebSocketConstructor=WebSocket}ws=new WebSocketConstructor(url,opts);ws.binaryType="arraybuffer"}catch(e){throw new FS.ErrnoError(23)}}var peer={addr,port,socket:ws,msg_send_queue:[]};SOCKFS.websocket_sock_ops.addPeer(sock,peer);SOCKFS.websocket_sock_ops.handlePeerEvents(sock,peer);if(sock.type===2&&typeof sock.sport!="undefined"){peer.msg_send_queue.push(new Uint8Array([255,255,255,255,"p".charCodeAt(0),"o".charCodeAt(0),"r".charCodeAt(0),"t".charCodeAt(0),(sock.sport&65280)>>8,sock.sport&255]))}return peer},getPeer(sock,addr,port){return sock.peers[addr+":"+port]},addPeer(sock,peer){sock.peers[peer.addr+":"+peer.port]=peer},removePeer(sock,peer){delete sock.peers[peer.addr+":"+peer.port]},handlePeerEvents(sock,peer){var first=true;var handleOpen=function(){sock.connecting=false;SOCKFS.emit("open",sock.stream.fd);try{var queued=peer.msg_send_queue.shift();while(queued){peer.socket.send(queued);queued=peer.msg_send_queue.shift()}}catch(e){peer.socket.close()}};function handleMessage(data){if(typeof data=="string"){var encoder=new TextEncoder;data=encoder.encode(data)}else{assert(data.byteLength!==undefined);if(data.byteLength==0){return}data=new Uint8Array(data)}var wasfirst=first;first=false;if(wasfirst&&data.length===10&&data[0]===255&&data[1]===255&&data[2]===255&&data[3]===255&&data[4]==="p".charCodeAt(0)&&data[5]==="o".charCodeAt(0)&&data[6]==="r".charCodeAt(0)&&data[7]==="t".charCodeAt(0)){var newport=data[8]<<8|data[9];SOCKFS.websocket_sock_ops.removePeer(sock,peer);peer.port=newport;SOCKFS.websocket_sock_ops.addPeer(sock,peer);return}sock.recv_queue.push({addr:peer.addr,port:peer.port,data});SOCKFS.emit("message",sock.stream.fd)}if(ENVIRONMENT_IS_NODE){peer.socket.on("open",handleOpen);peer.socket.on("message",function(data,isBinary){if(!isBinary){return}handleMessage(new Uint8Array(data).buffer)});peer.socket.on("close",function(){SOCKFS.emit("close",sock.stream.fd)});peer.socket.on("error",function(error){sock.error=14;SOCKFS.emit("error",[sock.stream.fd,sock.error,"ECONNREFUSED: Connection refused"])})}else{peer.socket.onopen=handleOpen;peer.socket.onclose=function(){SOCKFS.emit("close",sock.stream.fd)};peer.socket.onmessage=function peer_socket_onmessage(event){handleMessage(event.data)};peer.socket.onerror=function(error){sock.error=14;SOCKFS.emit("error",[sock.stream.fd,sock.error,"ECONNREFUSED: Connection refused"])}}},poll(sock){if(sock.type===1&&sock.server){return sock.pending.length?64|1:0}var mask=0;var dest=sock.type===1?SOCKFS.websocket_sock_ops.getPeer(sock,sock.daddr,sock.dport):null;if(sock.recv_queue.length||!dest||dest&&dest.socket.readyState===dest.socket.CLOSING||dest&&dest.socket.readyState===dest.socket.CLOSED){mask|=64|1}if(!dest||dest&&dest.socket.readyState===dest.socket.OPEN){mask|=4}if(dest&&dest.socket.readyState===dest.socket.CLOSING||dest&&dest.socket.readyState===dest.socket.CLOSED){if(sock.connecting){mask|=4}else{mask|=16}}return mask},ioctl(sock,request,arg){switch(request){case 21531:var bytes=0;if(sock.recv_queue.length){bytes=sock.recv_queue[0].data.length}HEAP32[arg>>2]=bytes;return 0;default:return 28}},close(sock){if(sock.server){try{sock.server.close()}catch(e){}sock.server=null}for(var peer of Object.values(sock.peers)){try{peer.socket.close()}catch(e){}SOCKFS.websocket_sock_ops.removePeer(sock,peer)}return 0},bind(sock,addr,port){if(typeof sock.saddr!="undefined"||typeof sock.sport!="undefined"){throw new FS.ErrnoError(28)}sock.saddr=addr;sock.sport=port;if(sock.type===2){if(sock.server){sock.server.close();sock.server=null}try{sock.sock_ops.listen(sock,0)}catch(e){if(!(e.name==="ErrnoError"))throw e;if(e.errno!==138)throw e}}},connect(sock,addr,port){if(sock.server){throw new FS.ErrnoError(138)}if(typeof sock.daddr!="undefined"&&typeof sock.dport!="undefined"){var dest=SOCKFS.websocket_sock_ops.getPeer(sock,sock.daddr,sock.dport);if(dest){if(dest.socket.readyState===dest.socket.CONNECTING){throw new FS.ErrnoError(7)}else{throw new FS.ErrnoError(30)}}}var peer=SOCKFS.websocket_sock_ops.createPeer(sock,addr,port);sock.daddr=peer.addr;sock.dport=peer.port;sock.connecting=true},listen(sock,backlog){if(!ENVIRONMENT_IS_NODE){throw new FS.ErrnoError(138)}if(sock.server){throw new FS.ErrnoError(28)}var WebSocketServer=require("ws").Server;var host=sock.saddr;sock.server=new WebSocketServer({host,port:sock.sport});SOCKFS.emit("listen",sock.stream.fd);sock.server.on("connection",function(ws){if(sock.type===1){var newsock=SOCKFS.createSocket(sock.family,sock.type,sock.protocol);var peer=SOCKFS.websocket_sock_ops.createPeer(newsock,ws);newsock.daddr=peer.addr;newsock.dport=peer.port;sock.pending.push(newsock);SOCKFS.emit("connection",newsock.stream.fd)}else{SOCKFS.websocket_sock_ops.createPeer(sock,ws);SOCKFS.emit("connection",sock.stream.fd)}});sock.server.on("close",function(){SOCKFS.emit("close",sock.stream.fd);sock.server=null});sock.server.on("error",function(error){sock.error=23;SOCKFS.emit("error",[sock.stream.fd,sock.error,"EHOSTUNREACH: Host is unreachable"])})},accept(listensock){if(!listensock.server||!listensock.pending.length){throw new FS.ErrnoError(28)}var newsock=listensock.pending.shift();newsock.stream.flags=listensock.stream.flags;return newsock},getname(sock,peer){var addr,port;if(peer){if(sock.daddr===undefined||sock.dport===undefined){throw new FS.ErrnoError(53)}addr=sock.daddr;port=sock.dport}else{addr=sock.saddr||0;port=sock.sport||0}return{addr,port}},sendmsg(sock,buffer,offset,length,addr,port){if(sock.type===2){if(addr===undefined||port===undefined){addr=sock.daddr;port=sock.dport}if(addr===undefined||port===undefined){throw new FS.ErrnoError(17)}}else{addr=sock.daddr;port=sock.dport}var dest=SOCKFS.websocket_sock_ops.getPeer(sock,addr,port);if(sock.type===1){if(!dest||dest.socket.readyState===dest.socket.CLOSING||dest.socket.readyState===dest.socket.CLOSED){throw new FS.ErrnoError(53)}}if(ArrayBuffer.isView(buffer)){offset+=buffer.byteOffset;buffer=buffer.buffer}var data=buffer.slice(offset,offset+length);if(!dest||dest.socket.readyState!==dest.socket.OPEN){if(sock.type===2){if(!dest||dest.socket.readyState===dest.socket.CLOSING||dest.socket.readyState===dest.socket.CLOSED){dest=SOCKFS.websocket_sock_ops.createPeer(sock,addr,port)}}dest.msg_send_queue.push(data);return length}try{dest.socket.send(data);return length}catch(e){throw new FS.ErrnoError(28)}},recvmsg(sock,length){if(sock.type===1&&sock.server){throw new FS.ErrnoError(53)}var queued=sock.recv_queue.shift();if(!queued){if(sock.type===1){var dest=SOCKFS.websocket_sock_ops.getPeer(sock,sock.daddr,sock.dport);if(!dest){throw new FS.ErrnoError(53)}if(dest.socket.readyState===dest.socket.CLOSING||dest.socket.readyState===dest.socket.CLOSED){return null}throw new FS.ErrnoError(6)}throw new FS.ErrnoError(6)}var queuedLength=queued.data.byteLength||queued.data.length;var queuedOffset=queued.data.byteOffset||0;var queuedBuffer=queued.data.buffer||queued.data;var bytesRead=Math.min(length,queuedLength);var res={buffer:new Uint8Array(queuedBuffer,queuedOffset,bytesRead),addr:queued.addr,port:queued.port};if(sock.type===1&&bytesRead{var socket=SOCKFS.getSocket(fd);if(!socket)throw new FS.ErrnoError(8);return socket};var inetNtop4=addr=>(addr&255)+"."+(addr>>8&255)+"."+(addr>>16&255)+"."+(addr>>24&255);var inetNtop6=ints=>{var str="";var word=0;var longest=0;var lastzero=0;var zstart=0;var len=0;var i=0;var parts=[ints[0]&65535,ints[0]>>16,ints[1]&65535,ints[1]>>16,ints[2]&65535,ints[2]>>16,ints[3]&65535,ints[3]>>16];var hasipv4=true;var v4part="";for(i=0;i<5;i++){if(parts[i]!==0){hasipv4=false;break}}if(hasipv4){v4part=inetNtop4(parts[6]|parts[7]<<16);if(parts[5]===-1){str="::ffff:";str+=v4part;return str}if(parts[5]===0){str="::";if(v4part==="0.0.0.0")v4part="";if(v4part==="0.0.0.1")v4part="1";str+=v4part;return str}}for(word=0;word<8;word++){if(parts[word]===0){if(word-lastzero>1){len=0}lastzero=word;len++}if(len>longest){longest=len;zstart=word-longest+1}}for(word=0;word<8;word++){if(longest>1){if(parts[word]===0&&word>=zstart&&word{var family=HEAP16[sa>>1];var port=_ntohs(HEAPU16[sa+2>>1]);var addr;switch(family){case 2:if(salen!==16){return{errno:28}}addr=HEAP32[sa+4>>2];addr=inetNtop4(addr);break;case 10:if(salen!==28){return{errno:28}}addr=[HEAP32[sa+8>>2],HEAP32[sa+12>>2],HEAP32[sa+16>>2],HEAP32[sa+20>>2]];addr=inetNtop6(addr);break;default:return{errno:5}}return{family,addr,port}};var inetPton4=str=>{var b=str.split(".");for(var i=0;i<4;i++){var tmp=Number(b[i]);if(isNaN(tmp))return null;b[i]=tmp}return(b[0]|b[1]<<8|b[2]<<16|b[3]<<24)>>>0};var inetPton6=str=>{var words;var w,offset,z;var valid6regx=/^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i;var parts=[];if(!valid6regx.test(str)){return null}if(str==="::"){return[0,0,0,0,0,0,0,0]}if(str.startsWith("::")){str=str.replace("::","Z:")}else{str=str.replace("::",":Z:")}if(str.indexOf(".")>0){str=str.replace(new RegExp("[.]","g"),":");words=str.split(":");words[words.length-4]=Number(words[words.length-4])+Number(words[words.length-3])*256;words[words.length-3]=Number(words[words.length-2])+Number(words[words.length-1])*256;words=words.slice(0,words.length-2)}else{words=str.split(":")}offset=0;z=0;for(w=0;w{var info=readSockaddr(addrp,addrlen);if(info.errno)throw new FS.ErrnoError(info.errno);info.addr=DNS.lookup_addr(info.addr)||info.addr;return info};function ___syscall_connect(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocketFromFD(fd);var info=getSocketAddress(addr,addrlen);sock.sock_ops.connect(sock,info.addr,info.port);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_dup3(fd,newfd,flags){try{var old=SYSCALLS.getStreamFromFD(fd);if(old.fd===newfd)return-28;if(newfd<0||newfd>=FS.MAX_OPEN_FDS)return-8;var existing=FS.getStream(newfd);if(existing)FS.close(existing);return FS.dupStream(old,newfd).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fchmod(fd,mode){try{FS.fchmod(fd,mode);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fchownat(dirfd,path,owner,group,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;flags=flags&~256;path=SYSCALLS.calculateAt(dirfd,path);(nofollow?FS.lchown:FS.chown)(path,owner,group);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fdatasync(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function ___syscall_ftruncate64(fd,length){length=bigintToI53Checked(length);try{if(isNaN(length))return 61;FS.ftruncate(fd,length);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>3]=BigInt(id);HEAP64[dirp+pos+8>>3]=BigInt((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var writeSockaddr=(sa,family,addr,port,addrlen)=>{switch(family){case 2:addr=inetPton4(addr);zeroMemory(sa,16);if(addrlen){HEAP32[addrlen>>2]=16}HEAP16[sa>>1]=family;HEAP32[sa+4>>2]=addr;HEAP16[sa+2>>1]=_htons(port);break;case 10:addr=inetPton6(addr);zeroMemory(sa,28);if(addrlen){HEAP32[addrlen>>2]=28}HEAP32[sa>>2]=family;HEAP32[sa+8>>2]=addr[0];HEAP32[sa+12>>2]=addr[1];HEAP32[sa+16>>2]=addr[2];HEAP32[sa+20>>2]=addr[3];HEAP16[sa+2>>1]=_htons(port);break;default:return 5}return 0};function ___syscall_getpeername(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocketFromFD(fd);if(!sock.daddr){return-53}var errno=writeSockaddr(addr,sock.family,DNS.lookup_name(sock.daddr),sock.dport,addrlen);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_getsockname(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocketFromFD(fd);var errno=writeSockaddr(addr,sock.family,DNS.lookup_name(sock.saddr||"0.0.0.0"),sock.sport,addrlen);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_getsockopt(fd,level,optname,optval,optlen,d1){try{var sock=getSocketFromFD(fd);if(level===1){if(optname===4){HEAP32[optval>>2]=sock.error;HEAP32[optlen>>2]=4;sock.error=null;return 0}}return-50}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_poll(fds,nfds,timeout){try{var nonzero=0;for(var i=0;i>2];var events=HEAP16[pollfd+4>>1];var mask=32;var stream=FS.getStream(fd);if(stream){mask=SYSCALLS.DEFAULT_POLLMASK;if(stream.stream_ops.poll){mask=stream.stream_ops.poll(stream,-1)}}mask&=events|8|16;if(mask)nonzero++;HEAP16[pollfd+6>>1]=mask}return nonzero}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_readlinkat(dirfd,path,buf,bufsize){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_recvfrom(fd,buf,len,flags,addr,addrlen){try{var sock=getSocketFromFD(fd);var msg=sock.sock_ops.recvmsg(sock,len);if(!msg)return 0;if(addr){var errno=writeSockaddr(addr,sock.family,DNS.lookup_name(msg.addr),msg.port,addrlen)}HEAPU8.set(msg.buffer,buf);return msg.buffer.byteLength}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_sendto(fd,message,length,flags,addr,addr_len){try{var sock=getSocketFromFD(fd);if(!addr){return FS.write(sock.stream,HEAP8,message,length)}var dest=getSocketAddress(addr,addr_len);return sock.sock_ops.sendmsg(sock,HEAP8,message,length,dest.addr,dest.port)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_socket(domain,type,protocol){try{var sock=SOCKFS.createSocket(domain,type,protocol);return sock.stream.fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_symlinkat(target,dirfd,linkpath){try{target=SYSCALLS.getStr(target);linkpath=SYSCALLS.getStr(linkpath);linkpath=SYSCALLS.calculateAt(dirfd,linkpath);FS.symlink(target,linkpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var readI53FromI64=ptr=>HEAPU32[ptr>>2]+HEAP32[ptr+4>>2]*4294967296;function ___syscall_utimensat(dirfd,path,times,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path,true);var now=Date.now(),atime,mtime;if(!times){atime=now;mtime=now}else{var seconds=readI53FromI64(times);var nanoseconds=HEAP32[times+8>>2];if(nanoseconds==1073741823){atime=now}else if(nanoseconds==1073741822){atime=null}else{atime=seconds*1e3+nanoseconds/(1e3*1e3)}times+=16;seconds=readI53FromI64(times);nanoseconds=HEAP32[times+8>>2];if(nanoseconds==1073741823){mtime=now}else if(nanoseconds==1073741822){mtime=null}else{mtime=seconds*1e3+nanoseconds/(1e3*1e3)}}if((mtime??atime)!==null){FS.utime(path,atime,mtime)}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};function __localtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __msync_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;SYSCALLS.doMsync(addr,SYSCALLS.getStreamFromFD(fd),len,flags,offset);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriting=0;var flags=0;{var stream=SYSCALLS.getStreamFromFD(fd);var type=stream.tty?2:FS.isDir(stream.mode)?3:FS.isLink(stream.mode)?7:4}HEAP8[pbuf]=type;HEAP16[pbuf+2>>1]=flags;HEAP64[pbuf+8>>3]=BigInt(rightsBase);HEAP64[pbuf+16>>3]=BigInt(rightsInheriting);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _getaddrinfo=(node,service,hint,out)=>{var addr=0;var port=0;var flags=0;var family=0;var type=0;var proto=0;var ai;function allocaddrinfo(family,type,proto,canon,addr,port){var sa,salen,ai;var errno;salen=family===10?28:16;addr=family===10?inetNtop6(addr):inetNtop4(addr);sa=_malloc(salen);errno=writeSockaddr(sa,family,addr,port);assert(!errno);ai=_malloc(32);HEAP32[ai+4>>2]=family;HEAP32[ai+8>>2]=type;HEAP32[ai+12>>2]=proto;HEAPU32[ai+24>>2]=canon;HEAPU32[ai+20>>2]=sa;if(family===10){HEAP32[ai+16>>2]=28}else{HEAP32[ai+16>>2]=16}HEAP32[ai+28>>2]=0;return ai}if(hint){flags=HEAP32[hint>>2];family=HEAP32[hint+4>>2];type=HEAP32[hint+8>>2];proto=HEAP32[hint+12>>2]}if(type&&!proto){proto=type===2?17:6}if(!type&&proto){type=proto===17?2:1}if(proto===0){proto=6}if(type===0){type=1}if(!node&&!service){return-2}if(flags&~(1|2|4|1024|8|16|32)){return-1}if(hint!==0&&HEAP32[hint>>2]&2&&!node){return-1}if(flags&32){return-2}if(type!==0&&type!==1&&type!==2){return-7}if(family!==0&&family!==2&&family!==10){return-6}if(service){service=UTF8ToString(service);port=parseInt(service,10);if(isNaN(port)){if(flags&1024){return-2}return-8}}if(!node){if(family===0){family=2}if((flags&1)===0){if(family===2){addr=_htonl(2130706433)}else{addr=[0,0,0,_htonl(1)]}}ai=allocaddrinfo(family,type,proto,null,addr,port);HEAPU32[out>>2]=ai;return 0}node=UTF8ToString(node);addr=inetPton4(node);if(addr!==null){if(family===0||family===2){family=2}else if(family===10&&flags&8){addr=[0,0,_htonl(65535),addr];family=10}else{return-2}}else{addr=inetPton6(node);if(addr!==null){if(family===0||family===10){family=10}else{return-2}}}if(addr!=null){ai=allocaddrinfo(family,type,proto,node,addr,port);HEAPU32[out>>2]=ai;return 0}if(flags&4){return-2}node=DNS.lookup_name(node);addr=inetPton4(node);if(family===0){family=2}else if(family===10){addr=[0,0,_htonl(65535),addr]}ai=allocaddrinfo(family,type,proto,null,addr,port);HEAPU32[out>>2]=ai;return 0};var _getnameinfo=(sa,salen,node,nodelen,serv,servlen,flags)=>{var info=readSockaddr(sa,salen);if(info.errno){return-6}var port=info.port;var addr=info.addr;var overflowed=false;if(node&&nodelen){var lookup;if(flags&1||!(lookup=DNS.lookup_addr(addr))){if(flags&8){return-2}}else{addr=lookup}var numBytesWrittenExclNull=stringToUTF8(addr,node,nodelen);if(numBytesWrittenExclNull+1>=nodelen){overflowed=true}}if(serv&&servlen){port=""+port;var numBytesWrittenExclNull=stringToUTF8(port,serv,servlen);if(numBytesWrittenExclNull+1>=servlen){overflowed=true}}if(overflowed){return-12}return 0};function _my_bit_log2_size_t(){abort("missing function: my_bit_log2_size_t")}_my_bit_log2_size_t.stub=true;var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var allocateUTF8=stringToNewUTF8;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";if(ENVIRONMENT_IS_NODE){NODEFS.staticInit()}var wasmImports={__cxa_throw:___cxa_throw,__syscall__newselect:___syscall__newselect,__syscall_chmod:___syscall_chmod,__syscall_connect:___syscall_connect,__syscall_dup3:___syscall_dup3,__syscall_faccessat:___syscall_faccessat,__syscall_fchmod:___syscall_fchmod,__syscall_fchownat:___syscall_fchownat,__syscall_fcntl64:___syscall_fcntl64,__syscall_fdatasync:___syscall_fdatasync,__syscall_fstat64:___syscall_fstat64,__syscall_ftruncate64:___syscall_ftruncate64,__syscall_getcwd:___syscall_getcwd,__syscall_getdents64:___syscall_getdents64,__syscall_getpeername:___syscall_getpeername,__syscall_getsockname:___syscall_getsockname,__syscall_getsockopt:___syscall_getsockopt,__syscall_ioctl:___syscall_ioctl,__syscall_lstat64:___syscall_lstat64,__syscall_mkdirat:___syscall_mkdirat,__syscall_newfstatat:___syscall_newfstatat,__syscall_openat:___syscall_openat,__syscall_poll:___syscall_poll,__syscall_readlinkat:___syscall_readlinkat,__syscall_recvfrom:___syscall_recvfrom,__syscall_renameat:___syscall_renameat,__syscall_rmdir:___syscall_rmdir,__syscall_sendto:___syscall_sendto,__syscall_socket:___syscall_socket,__syscall_stat64:___syscall_stat64,__syscall_symlinkat:___syscall_symlinkat,__syscall_unlinkat:___syscall_unlinkat,__syscall_utimensat:___syscall_utimensat,_abort_js:__abort_js,_gmtime_js:__gmtime_js,_localtime_js:__localtime_js,_mmap_js:__mmap_js,_msync_js:__msync_js,_munmap_js:__munmap_js,_tzset_js:__tzset_js,clock_time_get:_clock_time_get,emscripten_date_now:_emscripten_date_now,emscripten_get_heap_max:_emscripten_get_heap_max,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,exit:_exit,fd_close:_fd_close,fd_fdstat_get:_fd_fdstat_get,fd_pread:_fd_pread,fd_pwrite:_fd_pwrite,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write,getaddrinfo:_getaddrinfo,getnameinfo:_getnameinfo,my_bit_log2_size_t:_my_bit_log2_size_t};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["__wasm_call_ctors"];var _mysql_real_connect=Module["_mysql_real_connect"]=wasmExports["mysql_real_connect"];var _mysql_server_init=Module["_mysql_server_init"]=wasmExports["mysql_server_init"];var _mysql_server_end=Module["_mysql_server_end"]=wasmExports["mysql_server_end"];var _mysql_query=Module["_mysql_query"]=wasmExports["mysql_query"];var _mysql_fetch_field=Module["_mysql_fetch_field"]=wasmExports["mysql_fetch_field"];var _mysql_get_server_info=Module["_mysql_get_server_info"]=wasmExports["mysql_get_server_info"];var _mysql_field_count=Module["_mysql_field_count"]=wasmExports["mysql_field_count"];var _mysql_insert_id=Module["_mysql_insert_id"]=wasmExports["mysql_insert_id"];var _mysql_info=Module["_mysql_info"]=wasmExports["mysql_info"];var _mysql_close=Module["_mysql_close"]=wasmExports["mysql_close"];var _mysql_free_result=Module["_mysql_free_result"]=wasmExports["mysql_free_result"];var _mysql_init=Module["_mysql_init"]=wasmExports["mysql_init"];var _mysql_select_db=Module["_mysql_select_db"]=wasmExports["mysql_select_db"];var _mysql_store_result=Module["_mysql_store_result"]=wasmExports["mysql_store_result"];var _mysql_affected_rows=Module["_mysql_affected_rows"]=wasmExports["mysql_affected_rows"];var _mysql_fetch_row=Module["_mysql_fetch_row"]=wasmExports["mysql_fetch_row"];var _mysql_fetch_lengths=Module["_mysql_fetch_lengths"]=wasmExports["mysql_fetch_lengths"];var _mysql_num_rows=Module["_mysql_num_rows"]=wasmExports["mysql_num_rows"];var _mysql_num_fields=Module["_mysql_num_fields"]=wasmExports["mysql_num_fields"];var _mysql_errno=Module["_mysql_errno"]=wasmExports["mysql_errno"];var _mysql_error=Module["_mysql_error"]=wasmExports["mysql_error"];var _mysql_real_escape_string=Module["_mysql_real_escape_string"]=wasmExports["mysql_real_escape_string"];var _htonl=wasmExports["htonl"];var _htons=wasmExports["htons"];var _ntohs=wasmExports["ntohs"];var _emscripten_builtin_memalign=wasmExports["emscripten_builtin_memalign"];var _malloc=Module["_malloc"]=wasmExports["malloc"];var _free=Module["_free"]=wasmExports["free"];var __emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];var __emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];var _emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["getValue"]=getValue;Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["FS"]=FS;Module["allocateUTF8"]=allocateUTF8;Module["NODEFS"]=NODEFS;function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = createMariaDB; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = createMariaDB; +} else if (typeof define === 'function' && define['amd']) + define([], () => createMariaDB); diff --git a/packages/php-wasm/mariadb-wasm-compile/dist/mariadb.wasm b/packages/php-wasm/mariadb-wasm-compile/dist/mariadb.wasm new file mode 100755 index 00000000000..6c94adb92ac Binary files /dev/null and b/packages/php-wasm/mariadb-wasm-compile/dist/mariadb.wasm differ diff --git a/packages/php-wasm/mariadb-wasm-compile/package.json b/packages/php-wasm/mariadb-wasm-compile/package.json new file mode 100644 index 00000000000..b2fb0f25a80 --- /dev/null +++ b/packages/php-wasm/mariadb-wasm-compile/package.json @@ -0,0 +1,12 @@ +{ + "name": "@php-wasm/mariadb-wasm-compile", + "version": "0.1.0", + "private": true, + "description": "Compiles MariaDB 11.4 embedded server to WebAssembly via Emscripten", + "license": "GPL-2.0-or-later", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/wordpress-playground" + }, + "homepage": "https://developer.wordpress.org/playground" +} diff --git a/packages/php-wasm/mariadb-wasm-compile/project.json b/packages/php-wasm/mariadb-wasm-compile/project.json new file mode 100644 index 00000000000..5bd18e269f9 --- /dev/null +++ b/packages/php-wasm/mariadb-wasm-compile/project.json @@ -0,0 +1,16 @@ +{ + "name": "php-wasm-mariadb-wasm-compile", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/php-wasm/mariadb-wasm-compile", + "projectType": "library", + "tags": [], + "targets": { + "compile": { + "executor": "nx:run-commands", + "options": { + "command": "bash packages/php-wasm/mariadb-wasm-compile/build.sh", + "parallel": false + } + } + } +} diff --git a/packages/playground/cli/project.json b/packages/playground/cli/project.json index 3109c269019..1abb8ed947e 100644 --- a/packages/playground/cli/project.json +++ b/packages/playground/cli/project.json @@ -4,6 +4,10 @@ "sourceRoot": "packages/playground/cli/src", "projectType": "library", "tags": ["scope:php-wasm-public", "scope:cli"], + "implicitDependencies": [ + "playground-mariadb", + "php-wasm-mariadb-wasm-compile" + ], "targets": { "build": { "executor": "nx:noop", diff --git a/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts b/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts index b03fb70ef06..c60cb7df226 100644 --- a/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts +++ b/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts @@ -125,6 +125,21 @@ export class BlueprintsV1Handler { this.getEffectiveBlueprint() ); + // When using MariaDB WASM, define the MySQL credentials + // as PHP constants so WordPress connects to the protocol + // server instead of trying to use SQLite. + let constants = mergeDefinedConstants(this.args); + if (this.args.database === 'mariadb' && this.args.mariadbPort) { + constants = { + ...constants, + DB_HOST: `127.0.0.1:${this.args.mariadbPort}`, + DB_USER: 'root', + DB_PASSWORD: '', + DB_NAME: 'wordpress', + }; + } + + // TODO: Fix this type issue that requires the cast to unknown // TODO: Fix this type issue that requires the cast to unknown await ( playground as unknown as PlaygroundCliBlueprintV1Worker @@ -138,7 +153,7 @@ export class BlueprintsV1Handler { wordPressZip && (await wordPressZip!.arrayBuffer()), sqliteIntegrationPluginZip: await sqliteIntegrationPluginZip?.arrayBuffer(), - constants: mergeDefinedConstants(this.args), + constants, }, workerPostInstallMountsPort ); diff --git a/packages/playground/cli/src/cli-output.ts b/packages/playground/cli/src/cli-output.ts index c8714d9c6ab..686125ad830 100644 --- a/packages/playground/cli/src/cli-output.ts +++ b/packages/playground/cli/src/cli-output.ts @@ -34,6 +34,7 @@ export interface ConfigSummary { /** All mounts (both manual and auto-detected). Auto-mounts have autoMounted: true */ mounts: Mount[]; blueprint?: string; + database?: string; } export class CLIOutput { @@ -120,6 +121,13 @@ export class CLIOutput { `${this.dim('PHP')} ${this.cyan(config.phpVersion)} ${this.dim('WordPress')} ${this.cyan(config.wpVersion)}` ); + // Database engine + if (config.database === 'mariadb') { + lines.push( + `${this.dim('Database')} ${this.cyan('MariaDB WASM')}` + ); + } + // Extensions const extensions: string[] = []; if (config.intl) extensions.push('intl'); diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 57157b93c99..0ecadc01ab4 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -80,6 +80,7 @@ import { PHPMYADMIN_INSTALL_PATH, } from '@wp-playground/tools'; import { jspi } from 'wasm-feature-detect'; +import type { MariaDBServer, MariaDBBridge } from '@wp-playground/mariadb'; // Inlined worker URLs for static analysis by downstream bundlers // These are replaced at build time by the Vite plugin in vite.config.ts @@ -227,6 +228,20 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { type: 'boolean', default: false, }, + database: { + describe: + 'Database engine to use. "sqlite" uses the bundled SQLite integration plugin. ' + + '"mariadb" starts a MariaDB WASM server in-process and connects WordPress via MySQL protocol.', + type: 'string', + choices: ['sqlite', 'mariadb'] as const, + default: 'sqlite', + }, + 'mariadb-wasm-module': { + describe: + 'Path to the mariadb.js Emscripten module produced by the mariadb-wasm build. ' + + 'Required when --database=mariadb. The mariadb.wasm file must be in the same directory.', + type: 'string', + }, // Hidden - Deprecated in favor of verbosity quiet: { describe: 'Do not output logs and progress messages.', @@ -442,6 +457,9 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { 'define-number': sharedOptions['define-number'], // Tools phpmyadmin: sharedOptions['phpmyadmin'], + // Database + database: sharedOptions['database'], + 'mariadb-wasm-module': sharedOptions['mariadb-wasm-module'], }; const buildSnapshotOnlyOptions: Record = { @@ -735,9 +753,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { currentError = currentError.cause as Error; } while (currentError instanceof Error); console.error( - '\x1b[1m' + - messageChain.join(' caused by: ') + - '\x1b[0m' + '\x1b[1m' + messageChain.join(' caused by: ') + '\x1b[0m' ); } } else { @@ -846,6 +862,15 @@ export interface RunCLIArgs { */ 'define-number'?: Record; + // --------- Database args ----------- + database?: 'sqlite' | 'mariadb'; + 'mariadb-wasm-module'?: string; + /** + * When --database=mariadb, the main thread starts a MySQL protocol + * server and stores its port here so workers can connect. + */ + mariadbPort?: number; + // --------- Blueprint V1 args ----------- skipSqliteSetup?: boolean; followSymlinks?: boolean; @@ -989,6 +1014,23 @@ export async function runCLI(args: RunCLIArgs): Promise { args.memcached = await jspi(); } + // When --database=mariadb is selected, skip SQLite setup and + // default to the in-repo pre-built WASM module if no custom + // path was provided. + if (args.database === 'mariadb') { + args.skipSqliteSetup = true; + if (!args['mariadb-wasm-module']) { + // Resolve the default module path relative to the repo root. + // The CLI source lives at packages/playground/cli/src/, so + // we go up four levels to reach the repo root. + const defaultPath = path.resolve( + path.dirname(new URL(import.meta.url).pathname), + '../../../../packages/php-wasm/mariadb-wasm-compile/dist/mariadb.js' + ); + args['mariadb-wasm-module'] = defaultPath; + } + } + // Setup phpMyAdmin if enabled. if (args.phpmyadmin) { if (true === args.phpmyadmin) { @@ -1026,6 +1068,7 @@ export async function runCLI(args: RunCLIArgs): Promise { ], blueprint: typeof args.blueprint === 'string' ? args.blueprint : undefined, + database: args.database, }); } @@ -1037,6 +1080,8 @@ export async function runCLI(args: RunCLIArgs): Promise { let wordPressReady = false; let isFirstRequest = true; + let mariadbServer: MariaDBServer | undefined; + let mariadbBridge: MariaDBBridge | undefined; const server = await startServer({ port: args.port @@ -1081,6 +1126,36 @@ export async function runCLI(args: RunCLIArgs): Promise { await createPlaygroundCliTempDir(tempDirNameDelimiter); logger.debug(`Native temp dir for VFS root: ${nativeDir.path}`); + // Start MariaDB WASM server if --database=mariadb. + // This must happen before worker threads are spawned so + // the MySQL port is available when PHP boots. The data + // directory is a subdir of the shared temp dir so MariaDB + // files persist alongside PHP's files for the session. + if (args.database === 'mariadb') { + const { loadMariaDBModule, startMySQLProtocolServer } = + await import('@wp-playground/mariadb'); + + const mariadbDataDir = path.join( + nativeDir.path, + 'mariadb-data' + ); + mkdirSync(mariadbDataDir); + + cliOutput.updateProgress('Starting MariaDB WASM'); + mariadbBridge = await loadMariaDBModule( + args['mariadb-wasm-module']!, + mariadbDataDir + ); + mariadbServer = await startMySQLProtocolServer({ + bridge: mariadbBridge, + defaultDatabase: 'wordpress', + }); + args.mariadbPort = mariadbServer.port; + logger.debug( + `MariaDB WASM server listening on port ${mariadbServer.port}` + ); + } + const IDEConfigName = 'WP Playground CLI - Listen for Xdebug'; // Always clean up any existing Playground files symlink in the project root. @@ -1388,6 +1463,12 @@ export async function runCLI(args: RunCLIArgs): Promise { server.closeAllConnections(); }); } + if (mariadbServer) { + await mariadbServer.close(); + } + if (mariadbBridge) { + mariadbBridge.destroy(); + } await nativeDir.cleanup(); }; diff --git a/packages/playground/mariadb/.eslintrc.json b/packages/playground/mariadb/.eslintrc.json new file mode 100644 index 00000000000..ee9ffa963c8 --- /dev/null +++ b/packages/playground/mariadb/.eslintrc.json @@ -0,0 +1,32 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": [ + "{projectRoot}/vite.config.{js,ts,mjs,mts}" + ] + } + ] + } + } + ] +} diff --git a/packages/playground/mariadb/package.json b/packages/playground/mariadb/package.json new file mode 100644 index 00000000000..f2c378c2179 --- /dev/null +++ b/packages/playground/mariadb/package.json @@ -0,0 +1,31 @@ +{ + "name": "@wp-playground/mariadb", + "version": "3.1.18", + "description": "MariaDB WASM integration for WordPress Playground", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/wordpress-playground" + }, + "homepage": "https://developer.wordpress.org/playground", + "author": "The WordPress contributors", + "license": "GPL-2.0-or-later", + "type": "module", + "main": "./index.cjs", + "module": "./index.js", + "types": "index.d.ts", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + }, + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public", + "directory": "../../../dist/packages/playground/mariadb" + } +} diff --git a/packages/playground/mariadb/project.json b/packages/playground/mariadb/project.json new file mode 100644 index 00000000000..7c15435c165 --- /dev/null +++ b/packages/playground/mariadb/project.json @@ -0,0 +1,63 @@ +{ + "name": "playground-mariadb", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/playground/mariadb/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "nx:noop", + "dependsOn": ["build:package-json"] + }, + "build:package-json": { + "executor": "@wp-playground/nx-extensions:package-json", + "options": { + "tsConfig": "packages/playground/mariadb/tsconfig.lib.json", + "outputPath": "dist/packages/playground/mariadb", + "buildTarget": "playground-mariadb:build:bundle:production" + } + }, + "build:bundle": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/packages/playground/mariadb" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + }, + "dependsOn": ["^build"] + }, + "package-for-self-hosting": { + "executor": "@wp-playground/nx-extensions:package-for-self-hosting", + "dependsOn": ["build"] + }, + "test": { + "executor": "nx:noop", + "dependsOn": ["test:vite"] + }, + "test:vite": { + "executor": "@nx/vitest:test", + "outputs": ["{workspaceRoot}/coverage/packages/playground/mariadb"], + "options": { + "passWithNoTests": true, + "reportsDirectory": "../../../coverage/packages/playground/mariadb" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "useFlatConfig": false, + "lintFilePatterns": ["packages/playground/mariadb/**/*.ts"], + "maxWarnings": 0 + } + } + } +} diff --git a/packages/playground/mariadb/src/index.ts b/packages/playground/mariadb/src/index.ts new file mode 100644 index 00000000000..de9e44bfbbc --- /dev/null +++ b/packages/playground/mariadb/src/index.ts @@ -0,0 +1,17 @@ +export { + MariaDBBridge, + MariaDBQueryError, + loadMariaDBModule, +} from './lib/mariadb-wasm-bridge'; +export type { + MariaDBEmscriptenModule, + ColumnInfo, + QueryResult, +} from './lib/mariadb-wasm-bridge'; +export { + startMySQLProtocolServer, +} from './lib/mysql-protocol-server'; +export type { + MariaDBServer, + MariaDBServerOptions, +} from './lib/mysql-protocol-server'; diff --git a/packages/playground/mariadb/src/lib/mariadb-wasm-bridge.spec.ts b/packages/playground/mariadb/src/lib/mariadb-wasm-bridge.spec.ts new file mode 100644 index 00000000000..45f7e183e13 --- /dev/null +++ b/packages/playground/mariadb/src/lib/mariadb-wasm-bridge.spec.ts @@ -0,0 +1,255 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { MariaDBBridge, MariaDBQueryError } from './mariadb-wasm-bridge'; +import type { MariaDBEmscriptenModule } from './mariadb-wasm-bridge'; + +/** + * Creates a mock Emscripten module that simulates the MariaDB C API. + * The mock tracks calls and returns sensible defaults so the bridge + * can be exercised without the real WASM binary. + */ +function createMockModule(): MariaDBEmscriptenModule { + const MOCK_CONN = 42; + const MOCK_RESULT = 100; + const MOCK_FIELD = 200; + + // Simple in-memory state to simulate query behavior + let hasResultSet = false; + let rowIndex = 0; + + // Simulated result data for SELECT queries + const mockRows = [ + ['1', 'hello'], + ['2', 'world'], + ]; + const mockColumns = ['id', 'name']; + + const cwrapFunctions: Record any> = { + mysql_server_init: vi.fn(() => 0), + mysql_server_end: vi.fn(), + mysql_init: vi.fn(() => MOCK_CONN), + mysql_real_connect: vi.fn(() => MOCK_CONN), + mysql_close: vi.fn(), + mysql_query: vi.fn((_conn: number, sql: string) => { + hasResultSet = sql.trim().toUpperCase().startsWith('SELECT'); + rowIndex = 0; + if (sql.includes('FORCE_ERROR')) { + return 1; // non-zero = error + } + return 0; + }), + mysql_store_result: vi.fn(() => { + return hasResultSet ? MOCK_RESULT : 0; + }), + mysql_fetch_row: vi.fn(() => { + if (rowIndex < mockRows.length) { + rowIndex++; + return 300 + rowIndex; // non-zero pointer + } + return 0; // end of rows + }), + mysql_num_fields: vi.fn(() => (hasResultSet ? 2 : 0)), + mysql_num_rows: vi.fn(() => (hasResultSet ? mockRows.length : 0)), + mysql_free_result: vi.fn(), + mysql_error: vi.fn(() => 'Forced test error'), + mysql_errno: vi.fn(() => 1064), + mysql_affected_rows: vi.fn(() => (hasResultSet ? 0 : 1)), + mysql_field_count: vi.fn(() => (hasResultSet ? 2 : 0)), + mysql_fetch_field: vi.fn(() => MOCK_FIELD), + mysql_fetch_lengths: vi.fn(() => 400), + mysql_real_escape_string: vi.fn(() => 0), + mysql_select_db: vi.fn(() => 0), + mysql_get_server_info: vi.fn(() => '10.11.6-MariaDB'), + mysql_insert_id: vi.fn(() => 0), + }; + + // Track which field we're on for column metadata + let fieldIndex = 0; + + return { + cwrap: vi.fn((name: string) => { + return cwrapFunctions[name] || vi.fn(() => 0); + }), + getValue: vi.fn((ptr: number) => { + // Column name pointer — return a non-zero value so the bridge + // calls UTF8ToString on it + if (ptr === MOCK_FIELD) { + fieldIndex++; + return 500 + fieldIndex; + } + // Column length (offset 28) + if (ptr === MOCK_FIELD + 28) return 255; + // Column flags (offset 64) + if (ptr === MOCK_FIELD + 64) return 0; + // Column decimals (offset 68) + if (ptr === MOCK_FIELD + 68) return 0; + // Column type (offset 76) — MYSQL_TYPE_VAR_STRING = 253 + if (ptr === MOCK_FIELD + 76) return 253; + + // Row field pointers — return non-zero for valid values + if (ptr >= 301 && ptr < 400) return 600; + + // Field lengths + if (ptr >= 400 && ptr < 500) return 5; + + return 0; + }), + UTF8ToString: vi.fn((ptr: number) => { + // Column names + if (ptr >= 501 && ptr <= 502) { + return mockColumns[(ptr - 501) % mockColumns.length]; + } + // Row values — return mock data based on field/row + if (ptr === 600) return 'mock_value'; + return ''; + }), + stringToUTF8: vi.fn(), + _malloc: vi.fn(() => 1000), + _free: vi.fn(), + FS: { + mkdir: vi.fn(), + mount: vi.fn(), + }, + // HEAP8 is needed by init() to build the argv array for + // mysql_server_init via Int32Array view. + HEAP8: { buffer: new ArrayBuffer(8192) }, + } as any; +} + +describe('MariaDBBridge', () => { + let mockModule: MariaDBEmscriptenModule; + let bridge: MariaDBBridge; + + beforeEach(() => { + mockModule = createMockModule(); + bridge = new MariaDBBridge(mockModule); + }); + + it('wraps all required MySQL C API functions via cwrap', () => { + const expectedFunctions = [ + 'mysql_server_init', + 'mysql_server_end', + 'mysql_init', + 'mysql_real_connect', + 'mysql_close', + 'mysql_query', + 'mysql_store_result', + 'mysql_fetch_row', + 'mysql_num_fields', + 'mysql_num_rows', + 'mysql_free_result', + 'mysql_error', + 'mysql_errno', + 'mysql_affected_rows', + 'mysql_field_count', + 'mysql_fetch_field', + 'mysql_fetch_lengths', + 'mysql_real_escape_string', + 'mysql_select_db', + 'mysql_get_server_info', + 'mysql_insert_id', + ]; + + const cwrapCalls = (mockModule.cwrap as any).mock.calls.map( + (c: any[]) => c[0] + ); + for (const fn of expectedFunctions) { + expect(cwrapCalls).toContain(fn); + } + }); + + it('throws when querying before init()', () => { + expect(() => bridge.query('SELECT 1')).toThrow( + 'MariaDB bridge not initialized' + ); + }); + + it('initializes the embedded server and opens a connection', () => { + bridge.init(); + + // mysql_server_init should have been called + const serverInit = (mockModule.cwrap as any).mock.results.find( + (r: any, i: number) => + (mockModule.cwrap as any).mock.calls[i][0] === + 'mysql_server_init' + ); + expect(serverInit).toBeDefined(); + }); + + it('does not double-initialize', () => { + bridge.init(); + bridge.init(); // should be a no-op + // No error thrown means success + }); + + it('executes a non-SELECT query and returns affected rows', () => { + bridge.init(); + const result = bridge.query('INSERT INTO t VALUES (1)'); + + expect(result.columns).toHaveLength(0); + expect(result.rows).toHaveLength(0); + expect(result.affectedRows).toBe(1); + }); + + it('executes a SELECT query and returns columns and rows', () => { + bridge.init(); + const result = bridge.query('SELECT id, name FROM t'); + + expect(result.columns).toHaveLength(2); + expect(result.columns[0].name).toBe('id'); + expect(result.columns[1].name).toBe('name'); + expect(result.rows).toHaveLength(2); + }); + + it('throws MariaDBQueryError on query failure', () => { + bridge.init(); + try { + bridge.query('SELECT FORCE_ERROR'); + expect.fail('Should have thrown'); + } catch (e) { + expect(e).toBeInstanceOf(MariaDBQueryError); + const err = e as MariaDBQueryError; + expect(err.errno).toBe(1064); + expect(err.sql).toBe('SELECT FORCE_ERROR'); + } + }); + + it('returns server info', () => { + bridge.init(); + expect(bridge.getServerInfo()).toBe('10.11.6-MariaDB'); + }); + + it('returns a fallback string before init', () => { + expect(bridge.getServerInfo()).toContain('not initialized'); + }); + + it('cleans up on destroy()', () => { + bridge.init(); + bridge.destroy(); + + // After destroy, querying should fail + expect(() => bridge.query('SELECT 1')).toThrow( + 'MariaDB bridge not initialized' + ); + }); + + it('destroy() is safe to call multiple times', () => { + bridge.init(); + bridge.destroy(); + bridge.destroy(); // should not throw + }); + + it('destroy() is safe to call without init()', () => { + bridge.destroy(); // should not throw + }); +}); + +describe('MariaDBQueryError', () => { + it('includes errno and sql in the error', () => { + const err = new MariaDBQueryError('bad syntax', 1064, 'SELECT ???'); + expect(err.name).toBe('MariaDBQueryError'); + expect(err.message).toBe('bad syntax'); + expect(err.errno).toBe(1064); + expect(err.sql).toBe('SELECT ???'); + expect(err).toBeInstanceOf(Error); + }); +}); diff --git a/packages/playground/mariadb/src/lib/mariadb-wasm-bridge.ts b/packages/playground/mariadb/src/lib/mariadb-wasm-bridge.ts new file mode 100644 index 00000000000..798d4a636c9 --- /dev/null +++ b/packages/playground/mariadb/src/lib/mariadb-wasm-bridge.ts @@ -0,0 +1,695 @@ +/** + * Bridge between JavaScript and the MariaDB embedded server compiled to + * WebAssembly via Emscripten. + * + * The mariadb-wasm project (https://github.com/adamziel/mariadb-wasm) + * compiles MariaDB's libmysqld into a WASM module that exposes the MySQL + * C API through Emscripten's cwrap(). This bridge provides a clean + * JavaScript interface on top of those raw C function wrappers. + * + * Key design constraints: + * - All queries are synchronous from the WASM perspective (cwrap calls + * block until the C function returns). + * - There is no InnoDB — only MyISAM and MEMORY storage engines. + * - The embedded server runs single-threaded; concurrent access from + * multiple connection handles is not safe. + */ + +/** + * The shape of the Emscripten module returned by createMariaDB(). + * We only declare the methods we use. + */ +export interface MariaDBEmscriptenModule { + cwrap( + name: string, + returnType: string | null, + argTypes: string[] + ): (...args: any[]) => any; + getValue(ptr: number, type: string): number; + UTF8ToString(ptr: number): string; + stringToUTF8(str: string, outPtr: number, maxBytes: number): void; + _malloc(size: number): number; + _free(ptr: number): void; + FS: { + mkdir(path: string): void; + mount(type: any, opts: any, mountpoint: string): void; + }; + NODEFS?: any; +} + +/** + * Wrapped MySQL C API functions. Each function corresponds to a + * mysql_* C function from libmysqld. + */ +interface MariaDBAPI { + mysql_server_init: (argc: number, argv: number, groups: number) => number; + mysql_server_end: () => void; + mysql_init: (mysql: number) => number; + mysql_real_connect: ( + mysql: number, + host: string | null, + user: string | null, + passwd: string | null, + db: string | null, + port: number, + unix_socket: string | null, + clientflag: number + ) => number; + mysql_close: (mysql: number) => void; + mysql_query: (mysql: number, query: string) => number; + mysql_store_result: (mysql: number) => number; + mysql_fetch_row: (result: number) => number; + mysql_num_fields: (result: number) => number; + mysql_num_rows: (result: number) => number; + mysql_free_result: (result: number) => void; + mysql_error: (mysql: number) => string; + mysql_errno: (mysql: number) => number; + mysql_affected_rows: (mysql: number) => number; + mysql_field_count: (mysql: number) => number; + mysql_fetch_field: (result: number) => number; + mysql_fetch_lengths: (result: number) => number; + mysql_real_escape_string: ( + mysql: number, + to: number, + from: number, + length: number + ) => number; + mysql_select_db: (mysql: number, db: string) => number; + mysql_get_server_info: (mysql: number) => string; + mysql_insert_id: (mysql: number) => number; +} + +export interface ColumnInfo { + name: string; + /** The MySQL column type constant (e.g. MYSQL_TYPE_LONG = 3). */ + type: number; + /** Maximum display width from the column definition. */ + length: number; + /** Column flags (NOT_NULL, PRIMARY_KEY, etc.). */ + flags: number; + /** Number of decimal places for numeric types. */ + decimals: number; +} + +export interface QueryResult { + /** Column metadata for SELECT-type queries. Empty for non-SELECT. */ + columns: ColumnInfo[]; + /** + * Row data as arrays of strings (or null for SQL NULL values). + * Empty for non-SELECT queries. + */ + rows: (string | null)[][]; + /** Number of rows affected by INSERT/UPDATE/DELETE. */ + affectedRows: number; + /** Auto-generated ID from the last INSERT. */ + insertId: number; + /** Number of warnings from the last query. */ + warningCount: number; +} + +/** + * Wraps a cwrap'd function that may return BigInt (when WASM_BIGINT=1) + * so it always returns a plain Number. C functions like mysql_affected_rows + * and mysql_insert_id return unsigned long long which becomes BigInt in + * JavaScript with WASM_BIGINT enabled. We need plain Numbers for Buffer + * operations and arithmetic in the protocol server. + */ +function wrapBigInt(fn: (...args: any[]) => any): (...args: any[]) => number { + return (...args: any[]) => { + const result = fn(...args); + return typeof result === 'bigint' ? Number(result) : result; + }; +} + +/** + * High-level interface to a MariaDB embedded server running in WASM. + */ +export class MariaDBBridge { + private module: MariaDBEmscriptenModule; + private api: MariaDBAPI; + private conn = 0; + private initialized = false; + + constructor(module: MariaDBEmscriptenModule) { + this.module = module; + this.api = this.wrapAPI(); + } + + private wrapAPI(): MariaDBAPI { + const m = this.module; + return { + mysql_server_init: m.cwrap('mysql_server_init', 'number', [ + 'number', + 'number', + 'number', + ]), + mysql_server_end: m.cwrap('mysql_server_end', null, []), + mysql_init: m.cwrap('mysql_init', 'number', ['number']), + mysql_real_connect: m.cwrap('mysql_real_connect', 'number', [ + 'number', + 'string', + 'string', + 'string', + 'string', + 'number', + 'string', + 'number', + ]), + mysql_close: m.cwrap('mysql_close', null, ['number']), + mysql_query: m.cwrap('mysql_query', 'number', ['number', 'string']), + mysql_store_result: m.cwrap('mysql_store_result', 'number', [ + 'number', + ]), + mysql_fetch_row: m.cwrap('mysql_fetch_row', 'number', ['number']), + mysql_num_fields: m.cwrap('mysql_num_fields', 'number', ['number']), + mysql_num_rows: m.cwrap('mysql_num_rows', 'number', ['number']), + mysql_free_result: m.cwrap('mysql_free_result', null, ['number']), + mysql_error: m.cwrap('mysql_error', 'string', ['number']), + mysql_errno: m.cwrap('mysql_errno', 'number', ['number']), + // mysql_affected_rows returns unsigned long long. With + // WASM_BIGINT=1, cwrap returns BigInt which can't be + // used directly in arithmetic or Buffer operations. + // We wrap it to always return a plain Number. + mysql_affected_rows: wrapBigInt( + m.cwrap('mysql_affected_rows', 'number', ['number']) + ), + mysql_field_count: m.cwrap('mysql_field_count', 'number', [ + 'number', + ]), + mysql_fetch_field: m.cwrap('mysql_fetch_field', 'number', [ + 'number', + ]), + mysql_fetch_lengths: m.cwrap('mysql_fetch_lengths', 'number', [ + 'number', + ]), + mysql_real_escape_string: m.cwrap( + 'mysql_real_escape_string', + 'number', + ['number', 'number', 'number', 'number'] + ), + mysql_select_db: m.cwrap('mysql_select_db', 'number', [ + 'number', + 'string', + ]), + mysql_get_server_info: m.cwrap('mysql_get_server_info', 'string', [ + 'number', + ]), + // mysql_insert_id returns unsigned long long (see above). + mysql_insert_id: wrapBigInt( + m.cwrap('mysql_insert_id', 'number', ['number']) + ), + }; + } + + /** + * Initialize the embedded MariaDB server and open a connection. + * Must be called before any queries. + * + * @param dataDir - Optional host filesystem path for persistent + * storage. When provided, it's mounted via NODEFS at the MariaDB + * data directory so MyISAM files survive restarts. Without it, + * data lives in Emscripten's MEMFS and is lost on exit. + */ + init(dataDir?: string): void { + if (this.initialized) { + return; + } + + const DATA_DIR = '/mariadb-data'; + // Create the mount point and required subdirectories in + // Emscripten's virtual filesystem. + const requiredDirs = [DATA_DIR, DATA_DIR + '/mysql', '/tmp']; + for (const dir of requiredDirs) { + try { + this.module.FS.mkdir(dir); + } catch { + // Directory may already exist — that's fine. + } + } + + // When a host data directory is provided, mount it via NODEFS + // so MariaDB's data files persist on the real filesystem. The + // mount replaces the MEMFS directory we just created — any + // files already on the host will be visible to MariaDB. + if (dataDir && this.module.NODEFS) { + this.module.FS.mount( + this.module.NODEFS, + { root: dataDir }, + DATA_DIR + ); + } + + // Build the argv array for mysql_server_init. The embedded + // server parses these like mysqld command-line arguments. + const serverArgs = [ + 'mariadbd', + '--skip-grant-tables', + `--datadir=${DATA_DIR}`, + '--skip-log-error', + '--default-storage-engine=MyISAM', + '--default-tmp-storage-engine=MyISAM', + // Disable Aria for internal temp tables. Aria's init is + // stubbed in the WASM build (no threading), so any attempt + // to create Aria temp files fails. + '--loose-aria-used-for-temp-tables=OFF', + // Emscripten can't detect the WASM stack size, so MariaDB + // defaults thread_stack to 0 and rejects large queries. + '--thread-stack=1048576', + ]; + const argPtrs = serverArgs.map((arg) => { + const ptr = this.module._malloc(arg.length + 1); + this.module.stringToUTF8(arg, ptr, arg.length + 1); + return ptr; + }); + const argv = this.module._malloc(argPtrs.length * 4); + const heap32 = new Int32Array((this.module as any).HEAP8.buffer); + for (let i = 0; i < argPtrs.length; i++) { + heap32[(argv >> 2) + i] = argPtrs[i]; + } + + let rc: number; + try { + rc = this.api.mysql_server_init(serverArgs.length, argv, 0); + } finally { + // Free the argv strings and array now that mysql_server_init + // has copied what it needs. + for (const ptr of argPtrs) { + this.module._free(ptr); + } + this.module._free(argv); + } + if (rc !== 0) { + throw new Error(`mysql_server_init failed with code ${rc}`); + } + + this.conn = this.api.mysql_init(0); + if (this.conn === 0) { + throw new Error('mysql_init returned null'); + } + + // The embedded server ignores host/user/password — everything + // runs in-process. Passing nulls is correct here. + const connected = this.api.mysql_real_connect( + this.conn, + null, + null, + null, + null, + 0, + null, + 0 + ); + if (connected === 0) { + const err = this.api.mysql_error(this.conn); + throw new Error(`mysql_real_connect failed: ${err}`); + } + + this.initialized = true; + + // Bootstrap the mysql system database so MariaDB doesn't + // complain about missing privilege tables. Without this, + // every connection logs "Can't open and lock privilege tables." + this.bootstrapSystemTables(); + } + + /** + * Create the mysql system tables (global_priv, plugin, servers, etc.) + * that MariaDB expects to find on startup. This is the equivalent of + * running mysql_install_db. + * + * We run a minimal subset — just enough for the embedded server to + * operate without "Can't open and lock privilege tables" errors. + * The full bootstrap scripts use Aria-specific features that may + * not work with our stubbed Aria, so we use MyISAM explicitly. + */ + private bootstrapSystemTables(): void { + try { + this.query('CREATE DATABASE IF NOT EXISTS mysql'); + this.query('USE mysql'); + + // global_priv — the core privilege table + this.query(` + CREATE TABLE IF NOT EXISTS global_priv ( + Host char(255) binary DEFAULT '', + User char(128) binary DEFAULT '', + Priv JSON NOT NULL DEFAULT '{}' CHECK(JSON_VALID(Priv)), + PRIMARY KEY (Host, User) + ) ENGINE=MyISAM CHARACTER SET utf8mb3 COLLATE utf8mb3_bin + COMMENT='Users and global privileges' + `); + // Insert a root user with all privileges + this.query(` + INSERT IGNORE INTO global_priv (Host, User, Priv) + VALUES + ('localhost', 'root', '{"access":18446744073709551615}'), + ('127.0.0.1', 'root', '{"access":18446744073709551615}'), + ('%', 'root', '{"access":18446744073709551615}') + `); + + // plugin — plugin registry + this.query(` + CREATE TABLE IF NOT EXISTS plugin ( + name varchar(64) DEFAULT '' NOT NULL, + dl varchar(128) DEFAULT '' NOT NULL, + PRIMARY KEY (name) + ) ENGINE=MyISAM CHARACTER SET utf8mb3 + COLLATE utf8mb3_general_ci + COMMENT='MySQL plugins' + `); + + // servers — federated server links + this.query(` + CREATE TABLE IF NOT EXISTS servers ( + Server_name char(64) NOT NULL DEFAULT '', + Host varchar(2048) NOT NULL DEFAULT '', + Db char(64) NOT NULL DEFAULT '', + Username char(128) NOT NULL DEFAULT '', + Password char(64) NOT NULL DEFAULT '', + Port INT(4) NOT NULL DEFAULT '0', + Socket char(108) NOT NULL DEFAULT '', + Wrapper char(64) NOT NULL DEFAULT '', + Owner varchar(512) NOT NULL DEFAULT '', + PRIMARY KEY (Server_name) + ) ENGINE=MyISAM CHARACTER SET utf8mb3 + COMMENT='MySQL Foreign Servers table' + `); + + // func — user-defined functions + this.query(` + CREATE TABLE IF NOT EXISTS func ( + name char(64) binary DEFAULT '' NOT NULL, + ret tinyint(1) DEFAULT '0' NOT NULL, + dl char(128) DEFAULT '' NOT NULL, + type enum('function','aggregate') + COLLATE utf8mb3_general_ci NOT NULL, + PRIMARY KEY (name) + ) ENGINE=MyISAM CHARACTER SET utf8mb3 + COLLATE utf8mb3_bin + COMMENT='User defined functions' + `); + + // proc — stored procedures + this.query(` + CREATE TABLE IF NOT EXISTS proc ( + db char(64) collate utf8mb3_bin DEFAULT '' NOT NULL, + name char(64) DEFAULT '' NOT NULL, + type enum('FUNCTION','PROCEDURE','PACKAGE', + 'PACKAGE BODY') NOT NULL, + specific_name char(64) DEFAULT '' NOT NULL, + language enum('SQL') DEFAULT 'SQL' NOT NULL, + sql_data_access enum('CONTAINS_SQL','NO_SQL', + 'READS_SQL_DATA','MODIFIES_SQL_DATA') + DEFAULT 'CONTAINS_SQL' NOT NULL, + is_deterministic enum('YES','NO') + DEFAULT 'NO' NOT NULL, + security_type enum('INVOKER','DEFINER') + DEFAULT 'DEFINER' NOT NULL, + param_list blob NOT NULL, + returns longblob NOT NULL, + body longblob NOT NULL, + definer varchar(384) collate utf8mb3_bin + DEFAULT '' NOT NULL, + created timestamp NOT NULL DEFAULT + CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + modified timestamp NOT NULL + DEFAULT '0000-00-00 00:00:00', + sql_mode set('REAL_AS_FLOAT','PIPES_AS_CONCAT', + 'ANSI_QUOTES','IGNORE_SPACE', + 'IGNORE_BAD_TABLE_OPTIONS', + 'ONLY_FULL_GROUP_BY', + 'NO_UNSIGNED_SUBTRACTION', + 'NO_DIR_IN_CREATE','POSTGRESQL','ORACLE', + 'MSSQL','DB2','MAXDB','NO_KEY_OPTIONS', + 'NO_TABLE_OPTIONS','NO_FIELD_OPTIONS', + 'MYSQL323','MYSQL40','ANSI', + 'NO_AUTO_VALUE_ON_ZERO', + 'NO_BACKSLASH_ESCAPES', + 'STRICT_TRANS_TABLES','STRICT_ALL_TABLES', + 'NO_ZERO_IN_DATE','NO_ZERO_DATE', + 'INVALID_DATES', + 'ERROR_FOR_DIVISION_BY_ZERO', + 'TRADITIONAL','NO_AUTO_CREATE_USER', + 'HIGH_NOT_PRECEDENCE', + 'NO_ENGINE_SUBSTITUTION', + 'PAD_CHAR_TO_FULL_LENGTH', + 'EMPTY_STRING_IS_NULL', + 'SIMULTANEOUS_ASSIGNMENT', + 'TIME_ROUND_FRACTIONAL') + DEFAULT '' NOT NULL, + comment text collate utf8mb3_bin NOT NULL, + character_set_client char(32) + collate utf8mb3_bin, + collation_connection char(64) + collate utf8mb3_bin, + db_collation char(64) collate utf8mb3_bin, + body_utf8 longblob, + aggregate enum('NONE','GROUP') + DEFAULT 'NONE' NOT NULL, + PRIMARY KEY (db, name, type) + ) ENGINE=MyISAM CHARACTER SET utf8mb3 + COMMENT='Stored Procedures' + `); + } catch { + // Non-fatal: if bootstrap fails, MariaDB still works + // with --skip-grant-tables, just with warnings. + } + } + + /** + * Execute a SQL query and return structured results. + */ + query(sql: string): QueryResult { + if (!this.initialized) { + throw new Error( + 'MariaDB bridge not initialized. Call init() first.' + ); + } + + const rc = this.api.mysql_query(this.conn, sql); + if (rc !== 0) { + const errno = this.api.mysql_errno(this.conn); + const error = this.api.mysql_error(this.conn); + throw new MariaDBQueryError(error, errno, sql); + } + + const resultPtr = this.api.mysql_store_result(this.conn); + + // Non-SELECT statements (INSERT, UPDATE, DELETE, CREATE, etc.) + // return a null result set. + if (resultPtr === 0) { + return { + columns: [], + rows: [], + affectedRows: Math.max( + 0, + this.api.mysql_affected_rows(this.conn) + ), + insertId: Math.max(0, this.api.mysql_insert_id(this.conn)), + warningCount: 0, + }; + } + + try { + const numFields = this.api.mysql_num_fields(resultPtr); + const columns = this.readColumns(resultPtr, numFields); + const rows = this.readRows(resultPtr, numFields); + + return { + columns, + rows, + affectedRows: Math.max( + 0, + this.api.mysql_affected_rows(this.conn) + ), + insertId: Math.max(0, this.api.mysql_insert_id(this.conn)), + warningCount: 0, + }; + } finally { + this.api.mysql_free_result(resultPtr); + } + } + + /** + * Read column metadata from a result set. + * + * The MYSQL_FIELD struct layout (relevant fields): + * offset 0: char *name (pointer to column name) + * offset 4: char *org_name (pointer to original column name) + * offset 8: char *table (pointer to table name) + * offset 12: char *org_table (pointer to original table name) + * offset 16: char *db (pointer to database name) + * offset 20: char *catalog (pointer to catalog) + * offset 24: char *def (pointer to default value) + * offset 28: unsigned long length + * offset 32: unsigned long max_length + * offset 36: unsigned int name_length + * offset 40: unsigned int org_name_length + * offset 44: unsigned int table_length + * offset 48: unsigned int org_table_length + * offset 52: unsigned int db_length + * offset 56: unsigned int catalog_length + * offset 60: unsigned int def_length + * offset 64: unsigned int flags + * offset 68: unsigned int decimals + * offset 72: unsigned int charsetnr + * offset 76: enum enum_field_types type + * + * Note: These offsets assume 32-bit WASM (4-byte pointers). Emscripten + * compiles to wasm32 by default. + */ + private readColumns(resultPtr: number, numFields: number): ColumnInfo[] { + const columns: ColumnInfo[] = []; + for (let i = 0; i < numFields; i++) { + const fieldPtr = this.api.mysql_fetch_field(resultPtr); + if (fieldPtr === 0) { + columns.push({ + name: `col${i}`, + type: 253, // MYSQL_TYPE_VAR_STRING + length: 255, + flags: 0, + decimals: 0, + }); + continue; + } + + const namePtr = this.module.getValue(fieldPtr, 'i32'); + const name = namePtr + ? this.module.UTF8ToString(namePtr) + : `col${i}`; + // Read as signed i32 then interpret as unsigned via >>> 0. + // The MYSQL_FIELD struct uses unsigned long for length/flags + // but getValue only supports signed reads. + const length = this.module.getValue(fieldPtr + 28, 'i32') >>> 0; + const flags = this.module.getValue(fieldPtr + 64, 'i32') >>> 0; + const decimals = this.module.getValue(fieldPtr + 68, 'i32') >>> 0; + const type = this.module.getValue(fieldPtr + 76, 'i32'); + + columns.push({ name, type, length, flags, decimals }); + } + return columns; + } + + /** + * Read all rows from a result set. + * + * Each row is a char** (array of string pointers). We read + * pointer-sized values (4 bytes in wasm32) for each field. + */ + private readRows( + resultPtr: number, + numFields: number + ): (string | null)[][] { + const rows: (string | null)[][] = []; + let rowPtr: number; + while ((rowPtr = this.api.mysql_fetch_row(resultPtr)) !== 0) { + // Read field lengths for this row so we know the byte + // count of each value. mysql_fetch_lengths() returns a + // pointer to an unsigned long array (one entry per field). + const lengthsPtr = this.api.mysql_fetch_lengths(resultPtr); + + const row: (string | null)[] = []; + for (let i = 0; i < numFields; i++) { + const strPtr = this.module.getValue(rowPtr + i * 4, 'i32'); + if (strPtr === 0) { + row.push(null); + } else { + // Use the reported length rather than relying on + // null-termination. This correctly handles binary + // data that may contain embedded zero bytes. + const len = lengthsPtr + ? this.module.getValue(lengthsPtr + i * 4, 'i32') + : 0; + if (len > 0) { + row.push(this.module.UTF8ToString(strPtr)); + } else { + row.push(''); + } + } + } + rows.push(row); + } + return rows; + } + + /** + * Get the MariaDB server version string. + */ + getServerInfo(): string { + if (!this.initialized) { + return 'MariaDB WASM (not initialized)'; + } + return this.api.mysql_get_server_info(this.conn); + } + + /** + * Shut down the embedded server and release resources. + */ + destroy(): void { + if (!this.initialized) { + return; + } + try { + this.api.mysql_close(this.conn); + } catch { + // Ignore errors during shutdown + } + try { + this.api.mysql_server_end(); + } catch { + // Ignore errors during shutdown + } + this.conn = 0; + this.initialized = false; + } +} + +/** + * Error thrown when a MariaDB query fails. + */ +export class MariaDBQueryError extends Error { + errno: number; + sql: string; + + constructor(message: string, errno: number, sql: string) { + super(message); + this.name = 'MariaDBQueryError'; + this.errno = errno; + this.sql = sql; + } +} + +/** + * Load the mariadb-wasm Emscripten module from a file path. + * + * The path should point to the mariadb.js file produced by the + * mariadb-wasm build script. The corresponding mariadb.wasm file + * must be in the same directory. + * + * @param modulePath - Absolute path to mariadb.js + * @param dataDir - Optional host directory to mount as NODEFS at + * /var/lib/mysql inside the WASM filesystem. When + * provided, MariaDB's data files persist across + * restarts. + */ +export async function loadMariaDBModule( + modulePath: string, + dataDir?: string +): Promise { + // Dynamic import of the Emscripten-generated module. The module + // exports a factory function (createMariaDB) as its default export. + const factory = await importMariaDBFactory(modulePath); + const emModule: MariaDBEmscriptenModule = await factory({}); + + const bridge = new MariaDBBridge(emModule); + bridge.init(dataDir); + return bridge; +} + +async function importMariaDBFactory( + modulePath: string +): Promise< + (options?: Record) => Promise +> { + const imported = await import(/* webpackIgnore: true */ modulePath); + return imported.default || imported; +} diff --git a/packages/playground/mariadb/src/lib/mysql-protocol-server.spec.ts b/packages/playground/mariadb/src/lib/mysql-protocol-server.spec.ts new file mode 100644 index 00000000000..d05c69468b2 --- /dev/null +++ b/packages/playground/mariadb/src/lib/mysql-protocol-server.spec.ts @@ -0,0 +1,352 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as net from 'net'; +import { startMySQLProtocolServer } from './mysql-protocol-server'; +import type { MariaDBServer } from './mysql-protocol-server'; +import type { MariaDBBridge, QueryResult } from './mariadb-wasm-bridge'; +import { MariaDBQueryError } from './mariadb-wasm-bridge'; + +/** + * Create a minimal mock MariaDBBridge that responds to queries. + * We don't need the full Emscripten module — just the public API + * that the protocol server calls. + */ +function createMockBridge(): MariaDBBridge { + const bridge = { + getServerInfo: () => '10.11.6-MariaDB-mock', + query: (sql: string): QueryResult => { + const upper = sql.trim().toUpperCase(); + + if (upper.startsWith('SELECT')) { + return { + columns: [ + { + name: 'answer', + type: 253, + length: 255, + flags: 0, + decimals: 0, + }, + ], + rows: [['42']], + affectedRows: 0, + insertId: 0, + warningCount: 0, + }; + } + + if (sql.includes('FORCE_ERROR')) { + throw new MariaDBQueryError('test error', 1064, sql); + } + + return { + columns: [], + rows: [], + affectedRows: 1, + insertId: 0, + warningCount: 0, + }; + }, + } as unknown as MariaDBBridge; + return bridge; +} + +/** + * Read a complete MySQL packet from a buffer. + * Returns { payloadLength, sequenceId, payload, totalLength }. + */ +function readPacket(buf: Buffer) { + if (buf.length < 4) return null; + const payloadLength = buf.readUIntLE(0, 3); + const sequenceId = buf[3]; + if (buf.length < 4 + payloadLength) return null; + const payload = buf.subarray(4, 4 + payloadLength); + return { + payloadLength, + sequenceId, + payload, + totalLength: 4 + payloadLength, + }; +} + +/** + * Connect to the server and collect data until we have at least + * one complete packet, then return it. + */ +function connectAndReadHandshake( + port: number, + host: string +): Promise<{ socket: net.Socket; handshake: Buffer }> { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ port, host }, () => { + // Wait for the handshake packet + let buf = Buffer.alloc(0); + socket.on('data', function onData(data) { + buf = Buffer.concat([buf, data]); + const pkt = readPacket(buf); + if (pkt) { + socket.removeListener('data', onData); + resolve({ socket, handshake: buf }); + } + }); + }); + socket.on('error', reject); + }); +} + +/** + * Send a MySQL command packet and read the response. + */ +function sendCommand( + socket: net.Socket, + sequenceId: number, + payload: Buffer +): Promise { + return new Promise((resolve) => { + const header = Buffer.alloc(4); + header.writeUIntLE(payload.length, 0, 3); + header[3] = sequenceId & 0xff; + + let buf = Buffer.alloc(0); + function onData(data: Buffer) { + buf = Buffer.concat([buf, data]); + // Give it a moment to collect all packets for multi-packet responses + setTimeout(() => { + socket.removeListener('data', onData); + resolve(buf); + }, 50); + } + socket.on('data', onData); + socket.write(Buffer.concat([header, payload])); + }); +} + +/** + * Build a minimal Handshake Response 41 packet that the server will accept. + */ +function buildHandshakeResponse(database?: string): Buffer { + const parts: Buffer[] = []; + + // Capability flags (4 bytes) + let caps = (1 << 9) | (1 << 15); // CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION + if (database) { + caps |= 1 << 3; // CLIENT_CONNECT_WITH_DB + } + const capBuf = Buffer.alloc(4); + capBuf.writeUInt32LE(caps); + parts.push(capBuf); + + // Max packet size (4 bytes) + const maxPkt = Buffer.alloc(4); + maxPkt.writeUInt32LE(0x01000000); + parts.push(maxPkt); + + // Character set (1 byte) + parts.push(Buffer.from([33])); // utf8 + + // Reserved (23 zero bytes) + parts.push(Buffer.alloc(23)); + + // Username null-terminated + parts.push(Buffer.from('root\0', 'utf8')); + + // Auth response length + data (CLIENT_SECURE_CONNECTION) + parts.push(Buffer.from([0])); // 0-length auth + + // Database name if provided + if (database) { + parts.push(Buffer.from(database + '\0', 'utf8')); + } + + return Buffer.concat(parts); +} + +describe('MySQL Protocol Server', () => { + let server: MariaDBServer; + let bridge: MariaDBBridge; + + beforeEach(async () => { + bridge = createMockBridge(); + server = await startMySQLProtocolServer({ + bridge, + port: 0, // OS picks a free port + defaultDatabase: 'testdb', + }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('listens on the assigned port', () => { + expect(server.port).toBeGreaterThan(0); + expect(server.host).toBe('127.0.0.1'); + }); + + it('sends a valid handshake greeting on connect', async () => { + const { socket, handshake } = await connectAndReadHandshake( + server.port, + server.host + ); + + const pkt = readPacket(handshake)!; + expect(pkt.sequenceId).toBe(0); + + // Protocol version byte should be 10 + expect(pkt.payload[0]).toBe(10); + + // Server version string should contain "MariaDB" + const versionEnd = pkt.payload.indexOf(0, 1); + const version = pkt.payload.subarray(1, versionEnd).toString('utf8'); + expect(version).toContain('MariaDB'); + expect(version).toContain('playground-wasm'); + + socket.destroy(); + }); + + it('accepts a handshake response and returns OK', async () => { + const { socket } = await connectAndReadHandshake( + server.port, + server.host + ); + + const response = await sendCommand(socket, 1, buildHandshakeResponse()); + + const pkt = readPacket(response)!; + // OK packet starts with 0x00 + expect(pkt.payload[0]).toBe(0x00); + + socket.destroy(); + }); + + it('handles COM_PING with an OK response', async () => { + const { socket } = await connectAndReadHandshake( + server.port, + server.host + ); + + // Complete handshake + await sendCommand(socket, 1, buildHandshakeResponse()); + + // Send COM_PING (command byte 0x0e) + const pingResponse = await sendCommand(socket, 0, Buffer.from([0x0e])); + + const pkt = readPacket(pingResponse)!; + expect(pkt.payload[0]).toBe(0x00); // OK + + socket.destroy(); + }); + + it('handles COM_QUERY for a non-SELECT and returns OK', async () => { + const { socket } = await connectAndReadHandshake( + server.port, + server.host + ); + await sendCommand(socket, 1, buildHandshakeResponse()); + + // Send COM_QUERY with an INSERT statement + const queryPayload = Buffer.concat([ + Buffer.from([0x03]), // COM_QUERY + Buffer.from('INSERT INTO t VALUES (1)', 'utf8'), + ]); + const response = await sendCommand(socket, 0, queryPayload); + + const pkt = readPacket(response)!; + // OK packet + expect(pkt.payload[0]).toBe(0x00); + + socket.destroy(); + }); + + it('handles COM_QUERY for a SELECT and returns a result set', async () => { + const { socket } = await connectAndReadHandshake( + server.port, + server.host + ); + await sendCommand(socket, 1, buildHandshakeResponse()); + + const queryPayload = Buffer.concat([ + Buffer.from([0x03]), // COM_QUERY + Buffer.from('SELECT 42 AS answer', 'utf8'), + ]); + const response = await sendCommand(socket, 0, queryPayload); + + // The response should contain multiple packets: + // 1) column count, 2) column def, 3) EOF, 4) row data, 5) EOF + // First packet after header: column count + const pkt = readPacket(response)!; + // Column count should be 1 (length-encoded int) + expect(pkt.payload[0]).toBe(1); + + // The full response should contain "answer" (column name) and "42" (row value) + const responseStr = response.toString('utf8'); + expect(responseStr).toContain('answer'); + expect(responseStr).toContain('42'); + + socket.destroy(); + }); + + it('handles COM_QUERY errors with an error packet', async () => { + const { socket } = await connectAndReadHandshake( + server.port, + server.host + ); + await sendCommand(socket, 1, buildHandshakeResponse()); + + const queryPayload = Buffer.concat([ + Buffer.from([0x03]), + Buffer.from('FORCE_ERROR', 'utf8'), + ]); + const response = await sendCommand(socket, 0, queryPayload); + + const pkt = readPacket(response)!; + // Error packet starts with 0xFF + expect(pkt.payload[0]).toBe(0xff); + + // Error code at bytes 1-2 + const errno = pkt.payload.readUInt16LE(1); + expect(errno).toBe(1064); + + socket.destroy(); + }); + + it('handles COM_QUIT gracefully', async () => { + const { socket } = await connectAndReadHandshake( + server.port, + server.host + ); + await sendCommand(socket, 1, buildHandshakeResponse()); + + // Send COM_QUIT — the server should close the connection + const header = Buffer.alloc(4); + header.writeUIntLE(1, 0, 3); + header[3] = 0; + socket.write(Buffer.concat([header, Buffer.from([0x01])])); + + await new Promise((resolve) => { + socket.on('close', () => resolve()); + // Timeout fallback + setTimeout(() => { + socket.destroy(); + resolve(); + }, 500); + }); + }); + + it('close() shuts down the server', async () => { + await server.close(); + + // Trying to connect should fail + await expect( + new Promise((resolve, reject) => { + const s = net.createConnection( + { port: server.port, host: server.host }, + () => { + s.destroy(); + resolve(); + } + ); + s.on('error', reject); + }) + ).rejects.toThrow(); + }); +}); diff --git a/packages/playground/mariadb/src/lib/mysql-protocol-server.ts b/packages/playground/mariadb/src/lib/mysql-protocol-server.ts new file mode 100644 index 00000000000..4caafe62a19 --- /dev/null +++ b/packages/playground/mariadb/src/lib/mysql-protocol-server.ts @@ -0,0 +1,663 @@ +/** + * MySQL Wire Protocol Server + * + * Implements enough of the MySQL client/server protocol to let PHP's + * mysqli extension connect and run queries. The server accepts TCP + * connections, speaks the MySQL wire protocol, and forwards all SQL + * queries to a MariaDB WASM bridge (the embedded server compiled to + * WebAssembly). + * + * Protocol reference: + * https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basics.html + * + * We implement the minimum viable subset: + * - Initial handshake (server greeting + client auth response) + * - COM_QUERY (text protocol queries) + * - COM_INIT_DB (USE database) + * - COM_PING + * - COM_QUIT + * - COM_SET_OPTION + * - COM_FIELD_LIST (stub — WordPress probes this during init) + * + * Queries are serialized through Node's single-threaded event loop. + * Since mariadb-wasm C API calls are synchronous, the event loop + * blocks during query execution. This is fine for a development + * tool where queries take < 100ms. + */ + +import * as net from 'net'; +import type { + MariaDBBridge, + ColumnInfo, + QueryResult, +} from './mariadb-wasm-bridge'; +import { MariaDBQueryError } from './mariadb-wasm-bridge'; + +// -- MySQL protocol constants ----------------------------------------------- + +/** MySQL command byte constants. */ +const COM_QUIT = 0x01; +const COM_INIT_DB = 0x02; +const COM_QUERY = 0x03; +const COM_FIELD_LIST = 0x04; +const COM_PING = 0x0e; +const COM_SET_OPTION = 0x1b; + +/** Capability flags the server advertises. */ +const CLIENT_LONG_PASSWORD = 1; +const CLIENT_FOUND_ROWS = 1 << 1; +const CLIENT_LONG_FLAG = 1 << 2; +const CLIENT_CONNECT_WITH_DB = 1 << 3; +const CLIENT_PROTOCOL_41 = 1 << 9; +const CLIENT_SECURE_CONNECTION = 1 << 15; +const CLIENT_PLUGIN_AUTH = 1 << 19; + +const SERVER_CAPABILITIES = + CLIENT_LONG_PASSWORD | + CLIENT_FOUND_ROWS | + CLIENT_LONG_FLAG | + CLIENT_CONNECT_WITH_DB | + CLIENT_PROTOCOL_41 | + CLIENT_SECURE_CONNECTION | + CLIENT_PLUGIN_AUTH; + +/** Status flags included in OK/EOF packets. */ +const SERVER_STATUS_AUTOCOMMIT = 0x0002; + +// --------------------------------------------------------------------------- +// Packet-level encoding helpers +// --------------------------------------------------------------------------- + +/** + * Build a MySQL protocol packet: 3-byte length + 1-byte sequence ID + + * payload. Multiple payloads can be chained into one TCP write. + */ +function buildPacket(sequenceId: number, payload: Buffer): Buffer { + const header = Buffer.alloc(4); + header.writeUIntLE(payload.length, 0, 3); + header[3] = sequenceId & 0xff; + return Buffer.concat([header, payload]); +} + +/** Length-encoded integer (MySQL wire protocol encoding). */ +function encodeLenEncInt(value: number): Buffer { + // Guard against negative values from mysql_affected_rows / mysql_insert_id + // which return (unsigned long long) -1 in C but become -1 in JS without WASM_BIGINT. + if (value < 0) { + return Buffer.from([0]); + } + if (value < 251) { + return Buffer.from([value]); + } else if (value < 0x10000) { + const buf = Buffer.alloc(3); + buf[0] = 0xfc; + buf.writeUInt16LE(value, 1); + return buf; + } else if (value < 0x1000000) { + const buf = Buffer.alloc(4); + buf[0] = 0xfd; + buf.writeUIntLE(value, 1, 3); + return buf; + } else { + const buf = Buffer.alloc(9); + buf[0] = 0xfe; + // Write as two 32-bit halves for numbers that fit in a safe integer. + buf.writeUInt32LE(value & 0xffffffff, 1); + buf.writeUInt32LE(Math.floor(value / 0x100000000), 5); + return buf; + } +} + +/** Length-encoded string: length-encoded integer + raw bytes. */ +function encodeLenEncString(str: string): Buffer { + const strBuf = Buffer.from(str, 'utf8'); + return Buffer.concat([encodeLenEncInt(strBuf.length), strBuf]); +} + +// --------------------------------------------------------------------------- +// Packet builders +// --------------------------------------------------------------------------- + +/** + * Build the initial handshake packet the server sends when a client + * connects. This tells the client what protocol version we speak, + * what capabilities we have, and provides an auth challenge. + * + * We use mysql_native_password with a dummy 20-byte challenge because + * the embedded server has no real authentication — all connections are + * accepted as root. + */ +function buildHandshakePacket( + connectionId: number, + serverVersion: string +): Buffer { + const parts: Buffer[] = []; + + // Protocol version (always 10 for MySQL 4.1+). + parts.push(Buffer.from([10])); + + // Server version string, null-terminated. + parts.push(Buffer.from(serverVersion + '\0', 'utf8')); + + // Connection ID (4 bytes, little-endian). + const connIdBuf = Buffer.alloc(4); + connIdBuf.writeUInt32LE(connectionId); + parts.push(connIdBuf); + + // Auth challenge part 1 (8 bytes). We accept any auth response, + // so these can be arbitrary non-zero bytes. + const authChallenge1 = Buffer.from([ + 0x7a, 0x39, 0x5e, 0x22, 0x41, 0x6b, 0x3d, 0x17, + ]); + parts.push(authChallenge1); + + // Filler byte. + parts.push(Buffer.from([0x00])); + + // Lower 2 bytes of capability flags. + const capLow = Buffer.alloc(2); + capLow.writeUInt16LE(SERVER_CAPABILITIES & 0xffff); + parts.push(capLow); + + // Character set: utf8mb3 general ci = 33. + parts.push(Buffer.from([33])); + + // Status flags. + const statusBuf = Buffer.alloc(2); + statusBuf.writeUInt16LE(SERVER_STATUS_AUTOCOMMIT); + parts.push(statusBuf); + + // Upper 2 bytes of capability flags. + const capHigh = Buffer.alloc(2); + capHigh.writeUInt16LE((SERVER_CAPABILITIES >> 16) & 0xffff); + parts.push(capHigh); + + // Length of auth-plugin-data (21 for 8 + 13 bytes). + parts.push(Buffer.from([21])); + + // Reserved 10 bytes (zeros). + parts.push(Buffer.alloc(10)); + + // Auth challenge part 2 (13 bytes, including trailing null). + const authChallenge2 = Buffer.from([ + 0x50, 0x2a, 0x6f, 0x18, 0x55, 0x33, 0x7c, 0x24, 0x6e, 0x43, 0x5b, 0x09, + 0x00, + ]); + parts.push(authChallenge2); + + // Auth plugin name, null-terminated. + parts.push(Buffer.from('mysql_native_password\0', 'utf8')); + + return Buffer.concat(parts); +} + +/** OK packet for queries that don't return a result set. */ +function buildOKPacket( + affectedRows: number, + insertId: number, + warningCount: number +): Buffer { + const parts: Buffer[] = []; + // OK header byte. + parts.push(Buffer.from([0x00])); + parts.push(encodeLenEncInt(affectedRows)); + parts.push(encodeLenEncInt(insertId)); + // Status flags. + const status = Buffer.alloc(2); + status.writeUInt16LE(SERVER_STATUS_AUTOCOMMIT); + parts.push(status); + // Warnings. + const warn = Buffer.alloc(2); + warn.writeUInt16LE(warningCount); + parts.push(warn); + return Buffer.concat(parts); +} + +/** Error packet. */ +function buildErrorPacket( + errno: number, + message: string, + sqlState = 'HY000' +): Buffer { + const parts: Buffer[] = []; + // Error header byte. + parts.push(Buffer.from([0xff])); + // Error code (2 bytes). + const errBuf = Buffer.alloc(2); + errBuf.writeUInt16LE(errno); + parts.push(errBuf); + // SQL state marker '#' + 5 character state. + parts.push(Buffer.from('#' + sqlState.padEnd(5, '0'), 'utf8')); + // Human-readable error message. + parts.push(Buffer.from(message, 'utf8')); + return Buffer.concat(parts); +} + +/** EOF packet — signals the end of a sequence of column definitions or rows. */ +function buildEOFPacket(warningCount = 0): Buffer { + const buf = Buffer.alloc(5); + buf[0] = 0xfe; // EOF marker + buf.writeUInt16LE(warningCount, 1); + buf.writeUInt16LE(SERVER_STATUS_AUTOCOMMIT, 3); + return buf; +} + +/** + * Column definition packet (COM_QUERY response). + * + * This is the Protocol::ColumnDefinition41 packet that describes + * one column in a result set. + */ +function buildColumnDefinitionPacket(col: ColumnInfo, db: string): Buffer { + const parts: Buffer[] = []; + + // catalog (always "def"). + parts.push(encodeLenEncString('def')); + // schema (database name). + parts.push(encodeLenEncString(db)); + // table (virtual table — we don't track this, use empty string). + parts.push(encodeLenEncString('')); + // org_table. + parts.push(encodeLenEncString('')); + // name. + parts.push(encodeLenEncString(col.name)); + // org_name. + parts.push(encodeLenEncString(col.name)); + + // Length of fixed-length fields (always 0x0c). + parts.push(Buffer.from([0x0c])); + + const fixed = Buffer.alloc(12); + // Character set: utf8mb3 general ci = 33. + fixed.writeUInt16LE(33, 0); + // Column length. Clamp to unsigned 32-bit range — the C API may + // return negative values when interpreting unsigned fields without + // WASM_BIGINT. + fixed.writeUInt32LE(Math.max(0, col.length || 255) >>> 0, 2); + // Column type. + fixed[6] = col.type & 0xff; + // Flags. + fixed.writeUInt16LE((col.flags || 0) & 0xffff, 7); + // Decimals. + fixed[9] = col.decimals || 0; + // Filler (2 zero bytes) at offset 10 — already zero. + parts.push(fixed); + + return Buffer.concat(parts); +} + +/** + * Encode a result set row in the text protocol format. + * Each field is a length-encoded string, or 0xFB for NULL. + */ +function buildRowPacket(row: (string | null)[]): Buffer { + const parts: Buffer[] = []; + for (const value of row) { + if (value === null) { + parts.push(Buffer.from([0xfb])); + } else { + parts.push(encodeLenEncString(value)); + } + } + return Buffer.concat(parts); +} + +// --------------------------------------------------------------------------- +// Connection handler +// --------------------------------------------------------------------------- + +/** + * Per-connection state machine. Reads packets from the TCP stream, + * dispatches commands to the MariaDB bridge, and writes response + * packets back. + */ +class MySQLConnectionHandler { + private socket: net.Socket; + private bridge: MariaDBBridge; + private connectionId: number; + private currentDb: string; + private sequenceId = 0; + private buffer = Buffer.alloc(0); + + constructor( + socket: net.Socket, + bridge: MariaDBBridge, + connectionId: number, + defaultDb: string + ) { + this.socket = socket; + this.bridge = bridge; + this.connectionId = connectionId; + this.currentDb = defaultDb; + + socket.on('data', (data) => this.onData(data)); + socket.on('error', () => { + // Client disconnected ungracefully — nothing to do. + }); + + this.sendHandshake(); + } + + private sendHandshake() { + const serverVersion = this.bridge.getServerInfo() + '-playground-wasm'; + const payload = buildHandshakePacket(this.connectionId, serverVersion); + this.socket.write(buildPacket(0, payload)); + this.sequenceId = 1; + } + + private onData(data: Buffer) { + this.buffer = Buffer.concat([this.buffer, data]); + this.processBuffer(); + } + + /** + * Process complete packets from the buffer. A packet is complete + * when we have at least (payloadLength + 4) bytes. + */ + private processBuffer() { + while (this.buffer.length >= 4) { + const payloadLength = this.buffer.readUIntLE(0, 3); + const totalLength = payloadLength + 4; + + if (this.buffer.length < totalLength) { + // Need more data. + break; + } + + const sequenceId = this.buffer[3]; + const payload = this.buffer.subarray(4, totalLength); + this.buffer = this.buffer.subarray(totalLength); + + this.sequenceId = sequenceId; + this.handlePacket(payload); + } + } + + private handlePacket(payload: Buffer) { + // During the handshake phase (sequence 1), the first client + // packet is the Handshake Response. Accept it unconditionally. + if (this.sequenceId === 1) { + this.handleHandshakeResponse(payload); + return; + } + + if (payload.length === 0) { + return; + } + + const command = payload[0]; + const commandData = payload.subarray(1); + + switch (command) { + case COM_QUIT: + this.socket.end(); + break; + case COM_INIT_DB: + this.handleInitDB(commandData.toString('utf8')); + break; + case COM_QUERY: + this.handleQuery(commandData.toString('utf8')); + break; + case COM_PING: + this.sendOK(0, 0); + break; + case COM_FIELD_LIST: + // WordPress may send COM_FIELD_LIST during init. + // Respond with an immediate EOF (empty field list). + this.sendEOF(); + break; + case COM_SET_OPTION: + this.sendOK(0, 0); + break; + default: + this.sendError(1047, `Unknown command ${command}`, '08S01'); + } + } + + /** + * Accept any handshake response. The embedded server has no + * authentication — we're connecting to ourselves. + * + * We do inspect the packet for a database name (if the + * CLIENT_CONNECT_WITH_DB capability flag is set) and switch + * to that database. + */ + private handleHandshakeResponse(payload: Buffer) { + // Parse the Handshake Response 41 packet to extract the + // database name if provided. + if (payload.length >= 32) { + const capFlags = payload.readUInt32LE(0); + if (capFlags & CLIENT_CONNECT_WITH_DB) { + // Skip: 4 caps + 4 max_packet + 1 charset + 23 reserved = 32. + // Then: null-terminated username. + let offset = 32; + while (offset < payload.length && payload[offset] !== 0) { + offset++; + } + offset++; // skip null + + // Auth response (length-encoded or fixed based on caps). + if (capFlags & CLIENT_SECURE_CONNECTION) { + const authLen = payload[offset]; + offset += 1 + authLen; + } else { + while (offset < payload.length && payload[offset] !== 0) { + offset++; + } + offset++; + } + + // Database name (null-terminated). + if (offset < payload.length) { + const dbEnd = payload.indexOf(0, offset); + if (dbEnd > offset) { + const db = payload + .subarray(offset, dbEnd) + .toString('utf8'); + if (db) { + this.switchDatabase(db); + } + } + } + } + } + + this.sendOK(0, 0); + } + + private switchDatabase(db: string) { + // Escape backticks in the database name to prevent SQL injection. + const escaped = db.replace(/`/g, '``'); + try { + this.bridge.query(`CREATE DATABASE IF NOT EXISTS \`${escaped}\``); + this.bridge.query(`USE \`${escaped}\``); + this.currentDb = db; + } catch { + // Ignore errors — the database may already exist. + } + } + + private handleInitDB(db: string) { + try { + this.switchDatabase(db); + this.sendOK(0, 0); + } catch (e: any) { + this.sendError( + e.errno || 1049, + e.message || `Unknown database '${db}'` + ); + } + } + + private handleQuery(sql: string) { + try { + const result = this.bridge.query(sql); + if (result.columns.length === 0) { + // Non-SELECT query. + this.sendOK( + result.affectedRows, + result.insertId, + result.warningCount + ); + } else { + this.sendResultSet(result); + } + } catch (e: any) { + if (e instanceof MariaDBQueryError) { + this.sendError(e.errno, e.message); + } else { + this.sendError(1105, e.message || 'Unknown error'); + } + } + } + + /** + * Send a full result set response: + * 1. Column count packet + * 2. Column definition packets (one per column) + * 3. EOF packet + * 4. Row data packets + * 5. EOF packet + */ + private sendResultSet(result: QueryResult) { + let seq = this.sequenceId + 1; + const packets: Buffer[] = []; + + // Column count. + packets.push( + buildPacket(seq++, encodeLenEncInt(result.columns.length)) + ); + + // Column definitions. + for (const col of result.columns) { + packets.push( + buildPacket( + seq++, + buildColumnDefinitionPacket(col, this.currentDb) + ) + ); + } + + // EOF after columns. + packets.push(buildPacket(seq++, buildEOFPacket(result.warningCount))); + + // Row data. + for (const row of result.rows) { + packets.push(buildPacket(seq++, buildRowPacket(row))); + } + + // EOF after rows. + packets.push(buildPacket(seq++, buildEOFPacket(result.warningCount))); + + this.socket.write(Buffer.concat(packets)); + } + + private sendOK(affectedRows: number, insertId: number, warningCount = 0) { + const seq = this.sequenceId + 1; + this.socket.write( + buildPacket( + seq, + buildOKPacket(affectedRows, insertId, warningCount) + ) + ); + } + + private sendError(errno: number, message: string, sqlState = 'HY000') { + const seq = this.sequenceId + 1; + this.socket.write( + buildPacket(seq, buildErrorPacket(errno, message, sqlState)) + ); + } + + private sendEOF() { + const seq = this.sequenceId + 1; + this.socket.write(buildPacket(seq, buildEOFPacket())); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export interface MariaDBServerOptions { + /** The MariaDB WASM bridge to forward queries to. */ + bridge: MariaDBBridge; + /** TCP port to listen on. 0 = let the OS pick a free port. */ + port?: number; + /** Host to bind to. Defaults to '127.0.0.1'. */ + host?: string; + /** Default database name. Defaults to 'wordpress'. */ + defaultDatabase?: string; +} + +export interface MariaDBServer { + /** The TCP port the server is listening on. */ + port: number; + /** The host the server is bound to. */ + host: string; + /** Shut down the server and close all connections. */ + close(): Promise; +} + +/** + * Start a MySQL-compatible TCP server backed by MariaDB WASM. + * + * PHP's mysqli extension can connect to this server using standard + * MySQL credentials. The server forwards all queries to the MariaDB + * embedded server running in WebAssembly. + */ +export function startMySQLProtocolServer( + options: MariaDBServerOptions +): Promise { + const { + bridge, + port = 0, + host = '127.0.0.1', + defaultDatabase = 'wordpress', + } = options; + + // Ensure the default database exists. + try { + bridge.query(`CREATE DATABASE IF NOT EXISTS \`${defaultDatabase}\``); + bridge.query(`USE \`${defaultDatabase}\``); + } catch { + // Ignore — the bridge might not be ready yet, which is fine + // because each connection also creates the database. + } + + let nextConnectionId = 1; + const activeSockets = new Set(); + + const server = net.createServer((socket) => { + activeSockets.add(socket); + socket.on('close', () => activeSockets.delete(socket)); + new MySQLConnectionHandler( + socket, + bridge, + nextConnectionId++, + defaultDatabase + ); + }); + + return new Promise((resolve, reject) => { + server.on('error', reject); + server.listen(port, host, () => { + const addr = server.address() as net.AddressInfo; + resolve({ + port: addr.port, + host: addr.address, + close() { + return new Promise((res) => { + // Destroy all active connections so the + // server can shut down immediately. + for (const socket of activeSockets) { + socket.destroy(); + } + activeSockets.clear(); + server.close(() => res()); + }); + }, + }); + }); + }); +} diff --git a/packages/playground/mariadb/tsconfig.json b/packages/playground/mariadb/tsconfig.json new file mode 100644 index 00000000000..ad800198807 --- /dev/null +++ b/packages/playground/mariadb/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "ES2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/playground/mariadb/tsconfig.lib.json b/packages/playground/mariadb/tsconfig.lib.json new file mode 100644 index 00000000000..3383a9c93a8 --- /dev/null +++ b/packages/playground/mariadb/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/playground/mariadb/tsconfig.spec.json b/packages/playground/mariadb/tsconfig.spec.json new file mode 100644 index 00000000000..7b667033a4f --- /dev/null +++ b/packages/playground/mariadb/tsconfig.spec.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "src/*.test.ts", + "src/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/packages/playground/mariadb/vite.config.ts b/packages/playground/mariadb/vite.config.ts new file mode 100644 index 00000000000..45c93efec19 --- /dev/null +++ b/packages/playground/mariadb/vite.config.ts @@ -0,0 +1,45 @@ +/// +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { viteTsConfigPaths } from '../../vite-extensions/vite-ts-config-paths'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { getExternalModules } from '../../vite-extensions/vite-external-modules'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import viteGlobalExtensions from '../../vite-extensions/vite-global-extensions'; + +const toPath = (filename: string) => + new URL(filename, import.meta.url).pathname; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/playground-mariadb', + plugins: [ + viteTsConfigPaths({ root: '../../../' }), + dts({ + entryRoot: 'src', + tsconfigPath: toPath('tsconfig.lib.json'), + pathsToAliases: false, + }), + ...viteGlobalExtensions, + ], + build: { + lib: { + entry: 'src/index.ts', + name: 'playground-mariadb', + fileName: 'index', + formats: ['es', 'cjs'], + }, + sourcemap: true, + rollupOptions: { + external: getExternalModules(), + }, + }, + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + }, +}); diff --git a/packages/playground/wordpress/src/boot.ts b/packages/playground/wordpress/src/boot.ts index f38f21a0d19..ecbdbfa4b78 100644 --- a/packages/playground/wordpress/src/boot.ts +++ b/packages/playground/wordpress/src/boot.ts @@ -25,7 +25,7 @@ import { } from '.'; import { basename, dirname, joinPaths } from '@php-wasm/util'; import { logger } from '@php-wasm/logger'; -import { ensureWpConfig } from './wp-config'; +import { ensureWpConfig, defineWpConfigConstants } from './wp-config'; export type PhpIniOptions = Record; export type Hook = (php: PHP) => void | Promise; @@ -238,6 +238,34 @@ export async function bootWordPress( * definitions for some of the necessary constants. */ await ensureWpConfig(php, requestHandler.documentRoot); + + // When database credentials are provided as runtime constants (e.g. + // when using MariaDB WASM), also write them into wp-config.php so + // that pre-boot checks like hasValidMySQLCredentials() can find them + // by reading the file text. + if (options.constants) { + const dbKeys = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME']; + const dbConstants: Record = {}; + for (const key of dbKeys) { + if (key in options.constants) { + dbConstants[key] = options.constants[key]; + } + } + if (Object.keys(dbConstants).length > 0) { + const wpConfigPath = joinPaths( + requestHandler.documentRoot, + 'wp-config.php' + ); + if (php.fileExists(wpConfigPath)) { + await defineWpConfigConstants( + php, + wpConfigPath, + dbConstants + ); + } + } + } + // Run "before database" hooks to mount/copy more files in if (options.hooks?.beforeDatabaseSetup) { await options.hooks.beforeDatabaseSetup(php); diff --git a/tsconfig.base.json b/tsconfig.base.json index 164447419b7..bdfb7e2e5df 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -109,6 +109,9 @@ "@wp-playground/common": [ "packages/playground/common/src/index.ts" ], + "@wp-playground/mariadb": [ + "packages/playground/mariadb/src/index.ts" + ], "@wp-playground/mcp": ["packages/playground/mcp/src/index.ts"], "@wp-playground/mcp/client": [ "packages/playground/mcp/src/client.ts"