import { createHash } from "node:crypto"; import { constants as fsConstants, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile, } from "node:fs/promises"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { PHP, loadPHPRuntime } from "@php-wasm/universal"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); export const PHP_ENGINE_LOCK = Object.freeze({ bridgeAbi: 1, phpVersion: "8.5.8", pcreVersion: "10.44", packageVersion: "3.1.46", asyncMode: "asyncify", platform: "wasm32-emscripten", loaderBytes: 270_942, wasmBytes: 21_019_221, maximumPatternUtf16: 65_536, maximumSubjectBytes: 16_777_216, maximumReplacementUtf16: 65_536, maximumOutputBytes: 67_108_864, maximumMatches: 10_000, maximumCaptureRows: 100_000, maximumCaptureGroups: 1_000, npm: Object.freeze({ web: Object.freeze({ name: "@php-wasm/web-8-5", version: "3.1.46", resolved: "https://registry.npmjs.org/@php-wasm/web-8-5/-/web-8-5-3.1.46.tgz", integrity: "sha512-j9wwlY6aNXuKBr4JPJzqE+vkM3Onk+byJsxLqaJs4U0gYDUBLr8CuKuVr+p13apSTUosConePYTAs0214aNxYA==", packageJsonSha256: "bc69e9a9f88ed9c2e2c3ccd06ce7f842007cc0ef8edd4496a91b0131afdef1e4", }), universal: Object.freeze({ name: "@php-wasm/universal", version: "3.1.46", resolved: "https://registry.npmjs.org/@php-wasm/universal/-/universal-3.1.46.tgz", integrity: "sha512-sx0KyN5kcsCMzwy+XJAyoOukSA5TEZYd0LK3l9kRP3I0CSd5h6JItkf5eZ42WltB+wD+aTRdqSwc/Mvkc7qBjQ==", packageJsonSha256: "051ffd543901513ad76b5f935882b99988622e67086df16df48fef8f7c12a116", }), util: Object.freeze({ name: "@php-wasm/util", version: "3.1.46", resolved: "https://registry.npmjs.org/@php-wasm/util/-/util-3.1.46.tgz", integrity: "sha512-Yh9HNbx+rB2R1tScUvvAsbJVIB7msjWceWYogyR2YvAOqlDL0ymZyaQ65+K3QcHh/8HostTRwwgQh5oK4DLDrA==", packageJsonSha256: "4d2d42e0495a92e1fed76ac6273cf807d84466fe6dbd4718c5602012968d6e78", }), logger: Object.freeze({ name: "@php-wasm/logger", version: "3.1.46", resolved: "https://registry.npmjs.org/@php-wasm/logger/-/logger-3.1.46.tgz", integrity: "sha512-PfnDq5ammh5FXxkGX1TGEck9gmW3i76IvBCqtX4D45xnSBIuCeu8uf2oWuty2j/c8wTEO8vyRDdvCj5oow7bXw==", packageJsonSha256: "4e04ad8620452b48b1f225431c8143f4e14888408db2e3a166e02e4ac6863fc9", }), compression: Object.freeze({ name: "@php-wasm/stream-compression", version: "3.1.46", resolved: "https://registry.npmjs.org/@php-wasm/stream-compression/-/stream-compression-3.1.46.tgz", integrity: "sha512-+oUw4CPX52FgYXEh5C+MgJAg7Cy5OeaVuh7HnQR6oxb69nU63/r8tHTLV/WrWRyjBYh321uJzLV7Gg29qmiUtg==", packageJsonSha256: "5a2b0ca2dff1466bdeb38c9dca93d2fb0154cf9fef254a7c475d8cfe32344b72", }), progress: Object.freeze({ name: "@php-wasm/progress", version: "3.1.46", resolved: "https://registry.npmjs.org/@php-wasm/progress/-/progress-3.1.46.tgz", integrity: "sha512-WmmgEm6yz322M3pzgAUjxckyhpVxr2WWlHyVYDyaMZWU6UIDUHfiRZRPzADohNyYvV3g0/zMNaDGmxz+TTXwQQ==", packageJsonSha256: "0408ea4cdb2d251ad357090e9532f50769e9ca7c135aa823ccad9e7b95950a36", }), ini: Object.freeze({ name: "ini", version: "4.1.2", resolved: "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", integrity: "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", packageJsonSha256: "0bc65f73405054313cab76d1d5f89b888ad2c0e1261d3a97644728852a5df8dc", }), featureDetect: Object.freeze({ name: "wasm-feature-detect", version: "1.8.0", resolved: "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", integrity: "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", packageJsonSha256: "f511a8499a742e364d5bc9cdb23fb09108b12c7683fe06fb9e2bc324832b3b5b", }), }), sourceAssets: Object.freeze({ loader: "0ed44eae7cd040826775b3dcd5376bb86a14257ae9fe1ff5cda1b7daf7572088", wasm: "ed97ea5422dbf23e0687c98203cc8c6d7f2772a3d71b03d4991d2fdd93a048f0", nativeInventory: "c0da167d0a43b0d85f72ca17fed7d823c84b85718e4c8abf5f120f61f7c8654a", phpWasmLicense: "8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643", iniLicense: "4ec3d4c66cd87f5c8d8ad911b10f99bf27cb00cdfcff82621956e379186b016b", featureDetectLicense: "bcf29b4fd3ec2cb5f9d40a0866da446f6da62170d2ccedf4aeca9cf9406dd20c", }), legalAssets: Object.freeze({ "LICENSE.Emscripten-4.0.19.txt": "620a78084fc7ca97c0b5dea9abf891f3ffcadfdbf305276f099c9c4e12fc1d86", "LICENSE.Oniguruma-6.9.10.txt": "70ba5469ea0bab6e18a32d7009068f996503168d27be57747e08da34337ff26f", "LICENSE.OpenSSL-1.1.1t.txt": "c32913b33252e71190af2066f08115c69bc9fddadf3bf29296e20c835389841c", "LICENSE.PCRE2-10.44.txt": "030087e2e8dd7c1bdd26057d25d4ded8f45bbf01ad458d68665ad04b8b0fbedf", "LICENSE.PHP-4.0.txt": "e66a38e0a83f49b6ad6ec72f6c0d7600cfa35ba95ee1a67eeeae676cd4416789", "LICENSE.PHP-CLI-http-parser-MIT.txt": "8539e2b4bd4e874b0f7d94a25b8f1b445d46ae22c8373d5a69f801a1a12f3265", "LICENSE.PHP-Lexbor-Apache-2.0.txt": "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594", "LICENSE.PHP-Zend-2.0.txt": "509aeb3be64e428d7e0004ce224620ab01bf94adaacefbb449611939df56bc5b", "LICENSE.PHP-bcmath-LGPL-2.1.txt": "7586d794d062a2a4a16513d95358aecaee4450e2062f7e64ce5ce63ac7212fd6", "LICENSE.PHP-libavifinfo-BSD-2-Clause.txt": "5d40dcb72f934c3d08e10ddb8ba4844de17bf6f9aabc1a382aacf8a8bf4567f8", "LICENSE.PHP-libmagic-BSD.txt": "0bfa856a9930bddadbef95d1be1cf4e163c0be618e76ea3275caaf255283e274", "LICENSE.PHP-libmbfl-LGPL-2.1.txt": "31d9cadcc63c15b79fc59c70f043956104a34c75f23f0ac0ada53c1edfeb88ca", "LICENSE.PHP-timelib-MIT.txt": "2120d029e7c67b9d08c1f665f1c3ed581206f00970b664cccdebe1419bf566e9", "LICENSE.PHP-uriparser-BSD-3-Clause.txt": "287f09e6546a9610f949f89e8fb937cacfeabd7bfaa8c8a0c18312193bf04ad3", "LICENSE.curl-7.69.1.txt": "db3c4a3b3695a0f317a0c5176acd2f656d18abc45b3ee78e50935a78eb1e132e", "LICENSE.ini-ISC.txt": "4ec3d4c66cd87f5c8d8ad911b10f99bf27cb00cdfcff82621956e379186b016b", "LICENSE.libaom-3.12.1.txt": "4764a286d8b2faeaf42f4418e7d7a28d58fc8fd4d00a3d0a7f44b0a4099de7f2", "LICENSE.libavif-1.3.0.txt": "165abf92cc04b39e80d29cadea7a6a7e8fddf59407d4ad2616507a7ebe8216f9", "LICENSE.libcxx.txt": "539dd7aed86e8a4f12cbdd0e6c50c189c7d74847e4fecc64ce2c6ee3a01da38b", "LICENSE.libcxxabi.txt": "e2b35be49f7284a45b7baca8fc7b3ab7440e7902392b2528a457816b5bb2a15c", "LICENSE.libgd-2.3.3.txt": "005f4b6b0141d1bd11d371bbf7d4f67947f85a4906b7f5465f942204cf918ba3", "LICENSE.libiconv-1.17-LGPL-2.1.txt": "dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551", "LICENSE.libjpeg-turbo-3.0.3.txt": "e548c96c0c09397c94b5c5531836c02ff2c403f1f2b130cad8e3ec9439b5a7cc", "LICENSE.libpng-1.6.39.txt": "dfe5a536b0e5a531f844fb9c101a3089aca60772a503893b8e15f9457e369960", "LICENSE.libwebp.txt": "5aec868f669e384a22372a4e8a1a6cd7d44c64cd451f960ca69cc170d1e13acf", "LICENSE.libxml2-2.9.10.txt": "c5c63674f8a83c4d2e385d96d1c670a03cb871ba2927755467017317878574bd", "LICENSE.libyuv.txt": "2b2cc1180c7e6988328ad2033b04b80117419db9c4c584918bbb3cfec7e9364f", "LICENSE.libzip-1.9.2.txt": "01c022eca6d566e2e8792fd0f091a28653b2a608319922bcd4de91c49d1438e1", "LICENSE.llvm-compiler-rt.txt": "1a8f1058753f1ba890de984e48f0242a3a5c29a6a8f2ed9fd813f36985387e8d", "LICENSE.musl.txt": "f9bc4423732350eb0b3f7ed7e91d530298476f8fec0c6c427a1c04ade22655af", "LICENSE.php-wasm-GPL-2.0-or-later.txt": "8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643", "LICENSE.wasm-feature-detect-Apache-2.0.txt": "bcf29b4fd3ec2cb5f9d40a0866da446f6da62170d2ccedf4aeca9cf9406dd20c", "LICENSE.zlib-1.2.13.txt": "845efc77857d485d91fb3e0b884aaa929368c717ae8186b66fe1ed2495753243", "NOTICE.IANA-TZDATA-2026.1.txt": "8a19c6e511386301fa99ac61fec34c8be57be30255b29dad3ab294fe6b5da7c5", "NOTICE.PHP-Lexbor.txt": "b87f965fd2eba846a0a502d633dd7e7a680b93de5c514c404c948ccf1e5c9dc7", "NOTICE.PHP-REDIST-BINS.txt": "9159e8c784edf5ab9069c573bdc64a7a406ebe718768d2c25a0b243449b7665b", "NOTICE.PHP-public-domain-hash-code.txt": "1fc21940918bd2027d7b9ffccd23d45d35b33d6ca5b42a6fb481e75809763a98", "NOTICE.SQLite-3.51.0.txt": "b8262a33944a8937e6f38f573df2d945101b4bcb45de1b150c7e3a6ebd23b19b", "NOTICE.dlmalloc-2.8.6.txt": "ea9de40ffcf0d3536a71e22af9c33647c517256aa2a43167e933e74535569594", "PATENTS.PHP-libavifinfo.txt": "aef98cc6105f279463c37ec8c46dec5f2d133b6a91531ae9ec274e80fece9a88", "PATENTS.libaom-3.12.1.txt": "661fb8e504744e95587b556b94a58343448300606a41bea8c7a9b97125696e61", "PATENTS.libwebp.txt": "cc3273e0694ea5896145e0677699b53471b03ea43021ddc50e7923fbb9f5023c", }), }); const PACK_FILES = Object.freeze( [ ...Object.keys(PHP_ENGINE_LOCK.legalAssets), "SHA256SUMS", "engine-metadata.json", "native-components.json", "php-loader.mjs", "php.wasm", ].sort(), ); const CHECKSUM_FILES = Object.freeze( PACK_FILES.filter((name) => name !== "SHA256SUMS"), ); const LOADER_IMPORT = "import dependencyFilename from './8_5_8/php_8_5.wasm';"; const PATCHED_LOADER = 'const dependencyFilename = new URL("./php.wasm", import.meta.url).href;'; const PHP_NATIVE_COMPONENT_IDS = Object.freeze([ "wordpress-playground-php-wasm", "php", "pcre2", "php-libbcmath", "timelib", "iana-tzdata", "php-libmagic", "lexbor", "libmbfl", "php-uriparser", "php-libavifinfo", "zlib", "libzip", "libxml2", "sqlite", "libgd", "libjpeg-turbo", "libpng", "libwebp-and-libsharpyuv", "libavif", "libaom", "libyuv", "openssl", "curl", "libiconv", "oniguruma", "emscripten-runtime", "musl-libc", "llvm-compiler-rt", "llvm-libcxx", "llvm-libcxxabi", "dlmalloc", ]); const PHP_EXCLUDED_COMPONENT_IDS = Object.freeze([ "gmp", "libsodium", "tidy", "intl-and-icu", "freetype", "imagemagick-and-imagick", "standalone-libaom-3.13.1-recipe", ]); const PHP_LOADED_EXTENSIONS = Object.freeze( [ "Core", "date", "lexbor", "openssl", "pcre", "sqlite3", "zlib", "bcmath", "calendar", "ctype", "curl", "dns_polyfill", "uri", "json", "mbstring", "SPL", "fileinfo", "filter", "gd", "hash", "iconv", "session", "Zend OPcache", "standard", "libxml", "PDO", "pdo_sqlite", "Phar", "post_message_to_js", "random", "Reflection", "exif", "SimpleXML", "soap", "tokenizer", "dom", "wasm_memory_storage", "xml", "xmlreader", "xmlwriter", "zip", ].sort(compareText), ); const PHP_BINARY_MARKERS = Object.freeze([ "AOMedia Project AV1 Encoder v3.12.1", "AOMedia Project AV1 Decoder v3.12.1", "libjpeg-turbo version 3.0.3 (build 20250321)", ]); const PHP_EXCLUDED_BINARY_MARKERS = Object.freeze([ "AOMedia Project AV1 Encoder v3.13.1", "AOMedia Project AV1 Decoder v3.13.1", ]); function compareText(left, right) { return left < right ? -1 : left > right ? 1 : 0; } function sha256(value) { return createHash("sha256").update(value).digest("hex"); } async function sha256File(file) { return sha256(await readFile(file)); } async function assertRealDirectory(candidate, label) { const details = await lstat(candidate).catch(() => null); if (!details?.isDirectory() || details.isSymbolicLink()) { throw new Error(`${label} must be a real directory: ${candidate}`); } return realpath(candidate); } async function assertRegularFile(candidate, label) { const details = await lstat(candidate).catch(() => null); if (!details?.isFile() || details.isSymbolicLink()) { throw new Error( `${label} must be a regular, non-symlink file: ${candidate}`, ); } return realpath(candidate); } async function assertHash(file, expected, label) { const actual = await sha256File(file); if (actual !== expected) { throw new Error( `${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`, ); } } function assertExactJson(actual, expected, label) { if (JSON.stringify(actual) !== JSON.stringify(expected)) { throw new Error(`${label} drifted from the pinned PHP runtime inventory.`); } } function validateNativeInventory(inventory) { if ( inventory?.schemaVersion !== 1 || inventory.scope?.assetSha256 !== PHP_ENGINE_LOCK.sourceAssets.wasm || inventory.scope?.assetBytes !== PHP_ENGINE_LOCK.wasmBytes || inventory.scope?.phpVersion !== PHP_ENGINE_LOCK.phpVersion || inventory.scope?.platform !== PHP_ENGINE_LOCK.platform || inventory.buildProvenance?.wordpressPlayground?.release !== `v${PHP_ENGINE_LOCK.packageVersion}` || inventory.buildProvenance?.wordpressPlayground?.commit !== "581c7c172428159eb4e6c5309054a568cd39a97a" || inventory.buildProvenance?.php?.commit !== "26b97507444c4fbda072f57dda1820f7b7d5e467" || inventory.buildProvenance?.emscripten?.version !== "4.0.19" || inventory.buildProvenance?.emscripten?.commit !== "08e2de1031913e4ba7963b1c56f35f036a7d4d56" ) { throw new Error("PHP native-component provenance identity drifted."); } const componentIds = inventory.linkedComponents?.map(({ id }) => id); const excludedIds = inventory.excludedDetections?.map(({ id }) => id); assertExactJson( componentIds, PHP_NATIVE_COMPONENT_IDS, "PHP linked-component list", ); assertExactJson( excludedIds, PHP_EXCLUDED_COMPONENT_IDS, "PHP excluded-detection list", ); const referencedLegalFiles = new Set(); for (const component of inventory.linkedComponents) { if ( !Array.isArray(component.legalFiles) || component.legalFiles.length === 0 ) { throw new Error( `PHP native component ${component.id} has no legal-file route.`, ); } for (const name of component.legalFiles) { if (!Object.hasOwn(PHP_ENGINE_LOCK.legalAssets, name)) { throw new Error( `PHP native component ${component.id} references an unpinned legal file: ${name}.`, ); } referencedLegalFiles.add(name); } } for (const name of Object.keys(PHP_ENGINE_LOCK.legalAssets)) { if ( name !== "LICENSE.ini-ISC.txt" && name !== "LICENSE.wasm-feature-detect-Apache-2.0.txt" && !referencedLegalFiles.has(name) ) { throw new Error( `Pinned PHP native legal file is not routed from the inventory: ${name}.`, ); } } return inventory; } async function verifyPackage(projectRoot, descriptor) { const directory = path.join( projectRoot, "node_modules", ...descriptor.name.split("/"), ); await assertRealDirectory(directory, `${descriptor.name} npm package`); const packageFile = path.join(directory, "package.json"); await assertRegularFile(packageFile, `${descriptor.name} package.json`); await assertHash( packageFile, descriptor.packageJsonSha256, `${descriptor.name} package.json`, ); const packageJson = JSON.parse(await readFile(packageFile, "utf8")); if ( packageJson.name !== descriptor.name || packageJson.version !== descriptor.version ) { throw new Error(`${descriptor.name} npm package identity drifted.`); } return directory; } async function verifySources(projectRoot) { const lock = JSON.parse( await readFile(path.join(projectRoot, "package-lock.json"), "utf8"), ); const rootEntry = lock.packages?.[""]; for (const descriptor of Object.values(PHP_ENGINE_LOCK.npm)) { const entry = lock.packages?.[`node_modules/${descriptor.name}`]; if ( entry?.version !== descriptor.version || entry?.resolved !== descriptor.resolved || entry?.integrity !== descriptor.integrity ) { throw new Error( `${descriptor.name}@${descriptor.version} is not exactly pinned in package-lock.json.`, ); } await verifyPackage(projectRoot, descriptor); } for (const direct of [ PHP_ENGINE_LOCK.npm.web, PHP_ENGINE_LOCK.npm.universal, ]) { if (rootEntry?.devDependencies?.[direct.name] !== direct.version) { throw new Error(`${direct.name} must be an exact root devDependency.`); } } const web = path.join(projectRoot, "node_modules", "@php-wasm", "web-8-5"); const loader = path.join(web, "asyncify", "php_8_5.js"); const wasm = path.join(web, "asyncify", "8_5_8", "php_8_5.wasm"); const phpWasmLicense = path.join(web, "LICENSE"); const iniLicense = path.join(projectRoot, "node_modules", "ini", "LICENSE"); const featureDetectLicense = path.join( projectRoot, "node_modules", "wasm-feature-detect", "LICENSE", ); const engineSourceDirectory = path.join(projectRoot, "engines", "php"); const nativeInventory = path.join( engineSourceDirectory, "native-components.json", ); const legalFiles = Object.fromEntries( Object.keys(PHP_ENGINE_LOCK.legalAssets).map((name) => [ name, path.join(engineSourceDirectory, name), ]), ); Object.assign(legalFiles, { "LICENSE.php-wasm-GPL-2.0-or-later.txt": phpWasmLicense, "LICENSE.ini-ISC.txt": iniLicense, "LICENSE.wasm-feature-detect-Apache-2.0.txt": featureDetectLicense, }); for (const [file, expected, label] of [ [loader, PHP_ENGINE_LOCK.sourceAssets.loader, "PHP Asyncify loader"], [wasm, PHP_ENGINE_LOCK.sourceAssets.wasm, "PHP Asyncify WebAssembly"], [ nativeInventory, PHP_ENGINE_LOCK.sourceAssets.nativeInventory, "PHP native-component inventory", ], ]) { await assertRegularFile(file, label); await assertHash(file, expected, label); } for (const [name, expected] of Object.entries(PHP_ENGINE_LOCK.legalAssets)) { const file = legalFiles[name]; await assertRegularFile(file, `PHP legal asset ${name}`); await assertHash(file, expected, `PHP legal asset ${name}`); } validateNativeInventory(JSON.parse(await readFile(nativeInventory, "utf8"))); return { loader, wasm, nativeInventory, legalFiles, }; } function patchLoader(source) { if (!source.startsWith(`${LOADER_IMPORT}\n`)) { throw new Error("PHP Asyncify loader import prelude drifted."); } const patched = source.replace(LOADER_IMPORT, PATCHED_LOADER); if ( patched === source || patched.includes(LOADER_IMPORT) || !patched.includes("export function init(RuntimeName, PHPLoader)") ) { throw new Error("PHP Asyncify loader patch was not exact."); } return patched; } async function fileRecord(directory, name) { const value = await readFile(path.join(directory, name)); return { path: name, sha256: sha256(value), bytes: value.byteLength }; } async function expectedMetadata(projectRoot, pack) { const bridgePath = path.join(projectRoot, "engines", "php", "bridge.php"); const bridge = await readFile(bridgePath); const inventory = validateNativeInventory( JSON.parse( await readFile(path.join(pack, "native-components.json"), "utf8"), ), ); const inventoryRecord = await fileRecord(pack, "native-components.json"); const files = []; for (const name of CHECKSUM_FILES) { if (name !== "engine-metadata.json") { files.push(await fileRecord(pack, name)); } } return { schemaVersion: 1, engine: "php", engineVersion: `PHP ${PHP_ENGINE_LOCK.phpVersion} preg / PCRE2 ${PHP_ENGINE_LOCK.pcreVersion}`, runtime: `@php-wasm/web-8-5 ${PHP_ENGINE_LOCK.packageVersion} Asyncify`, platform: PHP_ENGINE_LOCK.platform, status: "production-runtime", bridge: { abiVersion: PHP_ENGINE_LOCK.bridgeAbi, sourceFile: { path: "engines/php/bridge.php", sha256: sha256(bridge), bytes: bridge.byteLength, }, transport: "fixed VFS script plus JSON request", userValuesAreJsonData: true, replacement: "bounded PHP 8.5.8 preg_get_backref-compatible expansion over native preg_match records", replacementParityOracle: "preg_replace is called only by the fixed identity self-test", replacementTransport: "bounded UTF-8 bytes encoded as base64 in the JSON envelope", nativeOffsetUnit: "UTF-8 bytes (PREG_OFFSET_CAPTURE)", maximumPatternUtf16: PHP_ENGINE_LOCK.maximumPatternUtf16, maximumSubjectBytes: PHP_ENGINE_LOCK.maximumSubjectBytes, maximumReplacementUtf16: PHP_ENGINE_LOCK.maximumReplacementUtf16, maximumOutputBytes: PHP_ENGINE_LOCK.maximumOutputBytes, maximumMatches: PHP_ENGINE_LOCK.maximumMatches, maximumCaptureRows: PHP_ENGINE_LOCK.maximumCaptureRows, maximumCaptureGroups: PHP_ENGINE_LOCK.maximumCaptureGroups, }, source: { repository: "https://github.com/WordPress/wordpress-playground", packages: Object.values(PHP_ENGINE_LOCK.npm).map( ({ name, version, integrity }) => ({ name, version, integrity }), ), runtimeVariant: "PHP 8.5 Asyncify web build (without intl extension)", loaderPatch: { originalSha256: PHP_ENGINE_LOCK.sourceAssets.loader, originalBytes: PHP_ENGINE_LOCK.loaderBytes, change: "replace bundler-only wasm import with same-directory php.wasm URL", }, php: { version: PHP_ENGINE_LOCK.phpVersion, pcreVersion: PHP_ENGINE_LOCK.pcreVersion, asyncMode: PHP_ENGINE_LOCK.asyncMode, platform: PHP_ENGINE_LOCK.platform, }, buildProvenance: { wordpressPlaygroundRelease: "v3.1.46", wordpressPlaygroundCommit: "581c7c172428159eb4e6c5309054a568cd39a97a", phpTag: "php-8.5.8", phpTagObject: "8a3b5a5124006c11a8fbfce838ec7dd53615cc77", phpCommit: "26b97507444c4fbda072f57dda1820f7b7d5e467", emscriptenVersion: "4.0.19", emscriptenCommit: "08e2de1031913e4ba7963b1c56f35f036a7d4d56", }, nativeComponents: { ...inventoryRecord, linkedComponentCount: inventory.linkedComponents.length, excludedDetectionCount: inventory.excludedDetections.length, scope: "complete native/source/legal inventory for the exact shipped php.wasm", }, embeddedPathClassification: "Strings such as /home/web_user in generated Emscripten artifacts are virtual/build filesystem paths, not host source provenance.", phpWasmLicenseAppliesTo: [ "@php-wasm/web-8-5", "@php-wasm/universal", "@php-wasm/util", "@php-wasm/logger", "@php-wasm/stream-compression", "@php-wasm/progress", ], }, licensing: { phpCodebase: { selectedLicense: "PHP License version 4 (BSD-3-Clause)", election: "PHP 8.5.8 is used under PHP License version 4 via the later-version option in section 5 of PHP License 3.01.", provenanceUrl: "https://www.php.net/license/", licenseTextUrl: "https://www.php.net/license/4_0.txt", licenseTextSha256: PHP_ENGINE_LOCK.legalAssets["LICENSE.PHP-4.0.txt"], }, pcre2: { version: PHP_ENGINE_LOCK.pcreVersion, license: "PCRE2 BSD license", provenanceUrl: "https://github.com/PCRE2Project/pcre2/blob/pcre2-10.44/LICENCE", licenseTextSha256: PHP_ENGINE_LOCK.legalAssets["LICENSE.PCRE2-10.44.txt"], }, phpWasmPackages: "GPL-2.0-or-later", ini: "ISC", wasmFeatureDetect: "Apache-2.0", nativeInventory: { path: inventoryRecord.path, sha256: inventoryRecord.sha256, independentlyPinnedLegalAssets: Object.keys(PHP_ENGINE_LOCK.legalAssets) .length, statement: "Every linked component routes to an independently SHA-256-pinned licence, notice, or patent file; raw-string false positives and unshipped side modules are explicitly excluded.", }, }, files, }; } async function runPhpRequest(php, request) { php.writeFile("/regex-tools/request.json", JSON.stringify(request)); const response = await php.run({ scriptPath: "/regex-tools/bridge.php" }); if ( response.exitCode !== 0 || response.errors !== "" || response.text.length === 0 ) { throw new Error("PHP bridge process failed its pack smoke test."); } return JSON.parse(response.text); } async function auditPhpRuntime(php) { const response = await php.run({ code: ` $extensions, 'versions' => [ 'php' => PHP_VERSION, 'pcre2' => PCRE_VERSION, 'openssl' => OPENSSL_VERSION_TEXT, 'curl' => curl_version()['version'], 'zlib' => ZLIB_VERSION, 'sqlite' => SQLite3::version()['versionString'], 'libxml2' => LIBXML_DOTTED_VERSION, 'libgd' => GD_VERSION, 'libiconv' => ICONV_VERSION, 'oniguruma' => MB_ONIGURUMA_VERSION, 'libzip' => ZipArchive::LIBZIP_VERSION, ], 'moduleMarkers' => [ 'timelib-2022.15' => str_contains($moduleInfo, '2022.15'), 'tzdata-2026.1' => str_contains($moduleInfo, '2026.1'), 'lexbor-2.7.0' => str_contains($moduleInfo, '2.7.0'), 'libmbfl-1.3.2' => str_contains($moduleInfo, '1.3.2'), 'uriparser-1.0.2' => str_contains($moduleInfo, '1.0.2'), ], 'gdFeatures' => [ 'jpeg' => $gd['JPEG Support'] ?? false, 'png' => $gd['PNG Support'] ?? false, 'webp' => $gd['WebP Support'] ?? false, 'avif' => $gd['AVIF Support'] ?? false, 'syntheticFreeTypeReport' => $gd['FreeType Support'] ?? false, ], 'hashAlgorithms' => array_fill_keys( ['fnv1a64', 'murmur3a', 'murmur3c', 'murmur3f', 'sha3-256', 'xxh32', 'xxh64', 'xxh3', 'xxh128'], false, ), 'phpZts' => (bool) PHP_ZTS, 'excluded' => [ 'gmp' => !extension_loaded('gmp') && !function_exists('gmp_init'), 'libsodium' => !extension_loaded('sodium') && !function_exists('sodium_crypto_box'), 'tidy' => !extension_loaded('tidy') && !function_exists('tidy_parse_string') && !class_exists('tidy'), 'intl-and-icu' => !extension_loaded('intl') && !class_exists('IntlDateFormatter'), 'imagemagick-and-imagick' => !extension_loaded('imagick') && !class_exists('Imagick'), ], ]; foreach (array_keys($audit['hashAlgorithms']) as $algorithm) { $audit['hashAlgorithms'][$algorithm] = in_array($algorithm, hash_algos(), true); } echo json_encode($audit, JSON_THROW_ON_ERROR);`, }); if ( response.exitCode !== 0 || response.errors !== "" || response.text.length === 0 ) { throw new Error("PHP native-component audit process failed."); } return JSON.parse(response.text); } async function smokePhp(pack, projectRoot) { const wasm = await readFile(path.join(pack, "php.wasm")); if ( wasm.byteLength !== PHP_ENGINE_LOCK.wasmBytes || !WebAssembly.validate(wasm) ) { throw new Error("PHP runtime is not the expected WebAssembly module."); } for (const marker of PHP_BINARY_MARKERS) { if (!wasm.includes(Buffer.from(marker, "utf8"))) { throw new Error(`PHP WebAssembly is missing native marker: ${marker}.`); } } for (const marker of PHP_EXCLUDED_BINARY_MARKERS) { if (wasm.includes(Buffer.from(marker, "utf8"))) { throw new Error(`PHP WebAssembly contains excluded marker: ${marker}.`); } } const loaderUrl = `${pathToFileURL(path.join(pack, "php-loader.mjs")).href}?verify=${Date.now()}-${Math.random()}`; const loader = await import(loaderUrl); if ( loader.dependenciesTotalSize !== PHP_ENGINE_LOCK.wasmBytes || typeof loader.init !== "function" ) { throw new Error("PHP Asyncify loader identity is invalid."); } const hadRequire = Object.hasOwn(globalThis, "require"); const previousRequire = globalThis.require; globalThis.require = createRequire(import.meta.url); let php; try { const runtimeId = await loadPHPRuntime(loader, { wasmBinary: wasm, phpWasmAsyncMode: "asyncify", debug: false, print: () => undefined, printErr: () => undefined, }); php = new PHP(runtimeId); php.mkdir("/regex-tools"); php.writeFile( "/regex-tools/bridge.php", await readFile( path.join(projectRoot, "engines", "php", "bridge.php"), "utf8", ), ); const identity = await runPhpRequest(php, { abi: PHP_ENGINE_LOCK.bridgeAbi, operation: "identity", }); if ( identity?.ok !== true || identity.identity?.phpVersion !== PHP_ENGINE_LOCK.phpVersion || identity.identity?.pcreVersion !== PHP_ENGINE_LOCK.pcreVersion || identity.identity?.phpSapi !== "wasm" || identity.identity?.integerBytes !== 8 || identity.identity?.selfTest !== true ) { throw new Error("PHP runtime failed its identity smoke test."); } const common = { abi: PHP_ENGINE_LOCK.bridgeAbi, pattern: "(?é+)", flags: "gu", options: {}, subject: "😀éé x", scanAll: true, maximumMatches: 10, maximumCaptureRows: 10, }; const execution = await runPhpRequest(php, { ...common, operation: "execute", }); if ( execution?.ok !== true || execution.groupCount !== 1 || JSON.stringify(execution.groupNames) !== JSON.stringify([[1, "word"]]) || JSON.stringify(execution.matches?.[0]?.span) !== JSON.stringify([4, 8]) ) { throw new Error("PHP runtime failed its native match smoke test."); } const replacement = await runPhpRequest(php, { ...common, operation: "replace", replacement: "[$1]", maximumOutputBytes: 1_024, }); if ( replacement?.ok !== true || Object.hasOwn(replacement, "output") || replacement.outputBase64 !== Buffer.from("😀[éé] x", "utf8").toString("base64") || Buffer.from(replacement.outputBase64, "base64").toString("utf8") !== "😀[éé] x" || replacement.outputTruncated !== false ) { throw new Error( "PHP runtime failed its bounded replacement transport smoke test.", ); } const audit = await auditPhpRuntime(php); assertExactJson( audit.extensions, PHP_LOADED_EXTENSIONS, "PHP loaded-extension list", ); assertExactJson( audit.versions, { php: "8.5.8", pcre2: "10.44 2024-06-07", openssl: "OpenSSL 1.1.1t 7 Feb 2023", curl: "7.69.1", zlib: "1.2.13", sqlite: "3.51.0", libxml2: "2.9.10", libgd: "2.3.3", libiconv: "1.17", oniguruma: "6.9.10", libzip: "1.9.2", }, "PHP linked-library versions", ); if ( Object.values(audit.moduleMarkers).some((present) => present !== true) || Object.values(audit.gdFeatures).some((present) => present !== true) || Object.values(audit.hashAlgorithms).some((present) => present !== true) || audit.phpZts !== false || Object.values(audit.excluded).some((excluded) => excluded !== true) ) { throw new Error( "PHP native-component positive/negative runtime audit failed.", ); } } finally { try { php?.exit(0); } catch { // The Emscripten runtime may signal its normal exit as an exception. } if (hadRequire) globalThis.require = previousRequire; else delete globalThis.require; } } export async function verifyPhpPack(directory, projectRoot = root) { const pack = await assertRealDirectory(directory, "PHP engine pack"); const names = (await readdir(pack, { withFileTypes: true })) .map((entry) => { if (!entry.isFile() || entry.isSymbolicLink()) { throw new Error(`PHP pack contains a non-regular entry: ${entry.name}`); } return entry.name; }) .sort(compareText); if (JSON.stringify(names) !== JSON.stringify(PACK_FILES)) { throw new Error(`PHP pack must contain only: ${PACK_FILES.join(", ")}.`); } const checksumLines = (await readFile(path.join(pack, "SHA256SUMS"), "utf8")) .trimEnd() .split("\n"); const expectedLines = []; for (const name of CHECKSUM_FILES) { expectedLines.push(`${await sha256File(path.join(pack, name))} ${name}`); } if (JSON.stringify(checksumLines) !== JSON.stringify(expectedLines)) { throw new Error("PHP pack checksum manifest is stale or malformed."); } await assertHash( path.join(pack, "php.wasm"), PHP_ENGINE_LOCK.sourceAssets.wasm, "Packed PHP WebAssembly", ); await assertHash( path.join(pack, "native-components.json"), PHP_ENGINE_LOCK.sourceAssets.nativeInventory, "Packed PHP native-component inventory", ); validateNativeInventory( JSON.parse( await readFile(path.join(pack, "native-components.json"), "utf8"), ), ); for (const [name, expected] of Object.entries(PHP_ENGINE_LOCK.legalAssets)) { await assertHash( path.join(pack, name), expected, `Packed PHP legal asset ${name}`, ); } const loader = await readFile(path.join(pack, "php-loader.mjs"), "utf8"); if ( !loader.startsWith(`${PATCHED_LOADER}\n`) || loader.includes(LOADER_IMPORT) ) { throw new Error("Packed PHP loader patch is invalid."); } const metadata = JSON.parse( await readFile(path.join(pack, "engine-metadata.json"), "utf8"), ); const expected = await expectedMetadata(projectRoot, pack); if (JSON.stringify(metadata) !== JSON.stringify(expected)) { throw new Error("PHP engine metadata does not match the pinned pack."); } await smokePhp(pack, projectRoot); return metadata; } export async function buildPhpPack({ projectRoot = root, output = path.join(root, ".engine-build", "php"), force = false, } = {}) { const sources = await verifySources(projectRoot); const target = path.resolve(output); const existing = await lstat(target).catch(() => null); if (existing && !force) { throw new Error(`${target} exists; pass --force to replace it.`); } if (existing && (existing.isSymbolicLink() || !existing.isDirectory())) { throw new Error("PHP pack output must be a real directory when replaced."); } await mkdir(path.dirname(target), { recursive: true }); const stage = await mkdtemp(path.join(path.dirname(target), ".php-pack-")); try { await copyFile( sources.wasm, path.join(stage, "php.wasm"), fsConstants.COPYFILE_EXCL, ); await copyFile( sources.nativeInventory, path.join(stage, "native-components.json"), fsConstants.COPYFILE_EXCL, ); for (const name of Object.keys(PHP_ENGINE_LOCK.legalAssets)) { await copyFile( sources.legalFiles[name], path.join(stage, name), fsConstants.COPYFILE_EXCL, ); } const loaderSource = await readFile(sources.loader, "utf8"); await writeFile( path.join(stage, "php-loader.mjs"), patchLoader(loaderSource), { flag: "wx", mode: 0o644 }, ); const metadata = await expectedMetadata(projectRoot, stage); await writeFile( path.join(stage, "engine-metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`, { flag: "wx", mode: 0o644 }, ); const checksumLines = []; for (const name of CHECKSUM_FILES) { checksumLines.push( `${await sha256File(path.join(stage, name))} ${name}`, ); } await writeFile( path.join(stage, "SHA256SUMS"), `${checksumLines.join("\n")}\n`, { flag: "wx", mode: 0o644 }, ); await verifyPhpPack(stage, projectRoot); if (existing) await rm(target, { recursive: true }); await rename(stage, target); } catch (error) { await rm(stage, { recursive: true, force: true }); throw error; } return { output: target, metadata: await verifyPhpPack(target, projectRoot), }; } export async function installPhpPack(source, projectRoot = root) { const sourcePath = await assertRealDirectory(source, "PHP staged pack"); const target = path.join(projectRoot, "public", "engines", "php"); if (sourcePath === (await realpath(target).catch(() => ""))) { throw new Error("PHP staged pack and installation target must differ."); } const metadata = await verifyPhpPack(sourcePath, projectRoot); await mkdir(path.dirname(target), { recursive: true }); const stage = await mkdtemp(path.join(path.dirname(target), ".php-install-")); let backup; try { for (const name of PACK_FILES) { await copyFile( path.join(sourcePath, name), path.join(stage, name), fsConstants.COPYFILE_EXCL, ); } await verifyPhpPack(stage, projectRoot); const existing = await lstat(target).catch(() => null); if (existing) { if (existing.isSymbolicLink() || !existing.isDirectory()) { throw new Error("Installed PHP pack target must be a real directory."); } backup = `${target}.backup-${process.pid}-${Date.now()}`; await rename(target, backup); } await rename(stage, target); if (backup) await rm(backup, { recursive: true }); } catch (error) { await rm(stage, { recursive: true, force: true }); if (backup && !(await lstat(target).catch(() => null))) { await rename(backup, target); } throw error; } return { output: target, metadata }; } function parseArguments(arguments_) { let output = path.join(root, ".engine-build", "php"); let force = false; for (let index = 0; index < arguments_.length; index += 1) { const argument = arguments_[index]; if (argument === "--force") { if (force) throw new Error("--force was provided more than once."); force = true; } else if (argument === "--output") { const value = arguments_[index + 1]; if (!value || value.startsWith("--")) { throw new Error("--output requires a directory."); } output = path.resolve(root, value); index += 1; } else { throw new Error(`Unknown PHP engine-pack argument: ${argument}`); } } return { output, force }; } async function main() { const result = await buildPhpPack(parseArguments(process.argv.slice(2))); console.log( `Built verified ${result.metadata.engineVersion} pack at ${result.output}.`, ); } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { await main(); }