diff options
Diffstat (limited to 'worker/php')
| -rw-r--r-- | worker/php/exec.mjs | 78 | ||||
| -rw-r--r-- | worker/php/justfile | 4 | ||||
| -rw-r--r-- | worker/php/lib.mjs | 79 | ||||
| -rw-r--r-- | worker/php/lib.test.mjs | 150 | ||||
| -rw-r--r-- | worker/php/package.json | 6 | ||||
| -rw-r--r-- | worker/php/vitest.config.mjs | 5 |
6 files changed, 250 insertions, 72 deletions
diff --git a/worker/php/exec.mjs b/worker/php/exec.mjs index d8ca899..21bd93f 100644 --- a/worker/php/exec.mjs +++ b/worker/php/exec.mjs @@ -1,65 +1,14 @@ import PHPWasm from "./php-wasm.js"; +import { buildResult, createIOCallbacks, preprocessCode } from "./lib.mjs"; process.once("message", async ({ code: originalCode, input }) => { - const PRELUDE = ` - define('STDIN', fopen('php://stdin', 'r')); - define('STDOUT', fopen('php://stdout', 'r')); - define('STDERR', fopen('php://stderr', 'r')); - - error_reporting(E_ALL & ~E_WARNING & ~E_NOTICE & ~E_DEPRECATED); - - `; - - // remove php tag - let code; - if (originalCode.startsWith("<?php")) { - code = PRELUDE + originalCode.slice(5); - } else if (originalCode.startsWith("<?")) { - code = PRELUDE + originalCode.slice(2); - } else { - code = PRELUDE + originalCode; - } - - const BUFFER_MAX = 10 * 1024; - - let stdinPos = 0; // bytewise - const stdinBuf = Buffer.from(input); - let stdoutPos = 0; // bytewise - const stdoutBuf = Buffer.alloc(BUFFER_MAX); - let stderrPos = 0; // bytewise - const stderrBuf = Buffer.alloc(BUFFER_MAX); + const code = preprocessCode(originalCode); + const io = createIOCallbacks(input); const { ccall } = await PHPWasm({ - stdin: () => { - if (stdinBuf.length <= stdinPos) { - return null; - } - return stdinBuf.readUInt8(stdinPos++); - }, - stdout: (asciiCode) => { - if (asciiCode === null) { - return; // flush - } - if (BUFFER_MAX <= stdoutPos) { - return; // ignore - } - stdoutBuf.writeUInt8( - asciiCode < 0 ? asciiCode + 256 : asciiCode, - stdoutPos++, - ); - }, - stderr: (asciiCode) => { - if (asciiCode === null) { - return; // flush - } - if (BUFFER_MAX <= stderrPos) { - return; // ignore - } - stderrBuf.writeUInt8( - asciiCode < 0 ? asciiCode + 256 : asciiCode, - stderrPos++, - ); - }, + stdin: io.stdin, + stdout: io.stdout, + stderr: io.stderr, }); let err; @@ -69,17 +18,6 @@ process.once("message", async ({ code: originalCode, input }) => { } catch (e) { err = e; } - if (err) { - process.send({ - status: "runtime_error", - stdout: stdoutBuf.subarray(0, stdoutPos).toString(), - stderr: `${stderrBuf.subarray(0, stderrPos).toString()}\n${err.toString()}`, - }); - } else { - process.send({ - status: result === 0 ? "success" : "runtime_error", - stdout: stdoutBuf.subarray(0, stdoutPos).toString(), - stderr: stderrBuf.subarray(0, stderrPos).toString(), - }); - } + + process.send(buildResult(err, result, io.getStdout, io.getStderr)); }); diff --git a/worker/php/justfile b/worker/php/justfile index 115165d..896798b 100644 --- a/worker/php/justfile +++ b/worker/php/justfile @@ -1,5 +1,9 @@ check: npm run check +test: + npm test + ci: npx biome ci . + npm test diff --git a/worker/php/lib.mjs b/worker/php/lib.mjs new file mode 100644 index 0000000..4a34733 --- /dev/null +++ b/worker/php/lib.mjs @@ -0,0 +1,79 @@ +const PRELUDE = ` + define('STDIN', fopen('php://stdin', 'r')); + define('STDOUT', fopen('php://stdout', 'r')); + define('STDERR', fopen('php://stderr', 'r')); + + error_reporting(E_ALL & ~E_WARNING & ~E_NOTICE & ~E_DEPRECATED); + + `; + +const BUFFER_MAX = 10 * 1024; + +export function preprocessCode(originalCode) { + if (originalCode.startsWith("<?php")) { + return PRELUDE + originalCode.slice(5); + } + if (originalCode.startsWith("<?")) { + return PRELUDE + originalCode.slice(2); + } + return PRELUDE + originalCode; +} + +export function createIOCallbacks(input) { + let stdinPos = 0; + const stdinBuf = Buffer.from(input); + let stdoutPos = 0; + const stdoutBuf = Buffer.alloc(BUFFER_MAX); + let stderrPos = 0; + const stderrBuf = Buffer.alloc(BUFFER_MAX); + + return { + stdin: () => { + if (stdinBuf.length <= stdinPos) { + return null; + } + return stdinBuf.readUInt8(stdinPos++); + }, + stdout: (asciiCode) => { + if (asciiCode === null) { + return; + } + if (BUFFER_MAX <= stdoutPos) { + return; + } + stdoutBuf.writeUInt8( + asciiCode < 0 ? asciiCode + 256 : asciiCode, + stdoutPos++, + ); + }, + stderr: (asciiCode) => { + if (asciiCode === null) { + return; + } + if (BUFFER_MAX <= stderrPos) { + return; + } + stderrBuf.writeUInt8( + asciiCode < 0 ? asciiCode + 256 : asciiCode, + stderrPos++, + ); + }, + getStdout: () => stdoutBuf.subarray(0, stdoutPos).toString(), + getStderr: () => stderrBuf.subarray(0, stderrPos).toString(), + }; +} + +export function buildResult(err, ccallResult, getStdout, getStderr) { + if (err) { + return { + status: "runtime_error", + stdout: getStdout(), + stderr: `${getStderr()}\n${err.toString()}`, + }; + } + return { + status: ccallResult === 0 ? "success" : "runtime_error", + stdout: getStdout(), + stderr: getStderr(), + }; +} diff --git a/worker/php/lib.test.mjs b/worker/php/lib.test.mjs new file mode 100644 index 0000000..6613066 --- /dev/null +++ b/worker/php/lib.test.mjs @@ -0,0 +1,150 @@ +import { describe, expect, it } from "vitest"; +import { buildResult, createIOCallbacks, preprocessCode } from "./lib.mjs"; + +describe("preprocessCode", () => { + it("removes <?php tag and prepends PRELUDE", () => { + const result = preprocessCode('<?php echo "hello";'); + expect(result).toContain('echo "hello";'); + expect(result).toContain("error_reporting"); + expect(result).not.toContain("<?php"); + }); + + it("removes <? short tag and prepends PRELUDE", () => { + const result = preprocessCode('<? echo "hello";'); + expect(result).toContain('echo "hello";'); + expect(result).toContain("error_reporting"); + expect(result).not.toContain("<?"); + }); + + it("prepends PRELUDE when no php tag present", () => { + const result = preprocessCode('echo "hello";'); + expect(result).toContain('echo "hello";'); + expect(result).toContain("error_reporting"); + }); + + it("handles empty string", () => { + const result = preprocessCode(""); + expect(result).toContain("error_reporting"); + }); + + it("does not remove <?php when not at the start", () => { + const result = preprocessCode('echo "x"; <?php echo "y";'); + expect(result).toContain("<?php"); + }); +}); + +describe("createIOCallbacks", () => { + describe("stdin", () => { + it("reads input byte by byte", () => { + const io = createIOCallbacks("AB"); + expect(io.stdin()).toBe(65); // 'A' + expect(io.stdin()).toBe(66); // 'B' + }); + + it("returns null at EOF", () => { + const io = createIOCallbacks("A"); + io.stdin(); // consume 'A' + expect(io.stdin()).toBeNull(); + expect(io.stdin()).toBeNull(); + }); + + it("returns null immediately for empty input", () => { + const io = createIOCallbacks(""); + expect(io.stdin()).toBeNull(); + }); + }); + + describe("stdout", () => { + it("captures ASCII writes", () => { + const io = createIOCallbacks(""); + io.stdout(72); // 'H' + io.stdout(105); // 'i' + expect(io.getStdout()).toBe("Hi"); + }); + + it("ignores null (flush)", () => { + const io = createIOCallbacks(""); + io.stdout(65); + io.stdout(null); + io.stdout(66); + expect(io.getStdout()).toBe("AB"); + }); + + it("corrects negative asciiCode by adding 256", () => { + const io = createIOCallbacks(""); + // -191 + 256 = 65 = 'A' + io.stdout(-191); + expect(io.getStdout()).toBe("A"); + }); + + it("truncates output at 10KB buffer limit", () => { + const io = createIOCallbacks(""); + const limit = 10 * 1024; + for (let i = 0; i < limit + 100; i++) { + io.stdout(65); + } + expect(io.getStdout().length).toBe(limit); + }); + }); + + describe("stderr", () => { + it("captures ASCII writes", () => { + const io = createIOCallbacks(""); + io.stderr(69); // 'E' + io.stderr(114); // 'r' + expect(io.getStderr()).toBe("Er"); + }); + + it("ignores null (flush)", () => { + const io = createIOCallbacks(""); + io.stderr(65); + io.stderr(null); + expect(io.getStderr()).toBe("A"); + }); + + it("corrects negative asciiCode by adding 256", () => { + const io = createIOCallbacks(""); + // -156 + 256 = 100 = 'd' + io.stderr(-156); + expect(io.getStderr()).toBe("d"); + }); + + it("truncates output at 10KB buffer limit", () => { + const io = createIOCallbacks(""); + const limit = 10 * 1024; + for (let i = 0; i < limit + 100; i++) { + io.stderr(65); + } + expect(io.getStderr().length).toBe(limit); + }); + }); +}); + +describe("buildResult", () => { + it("returns success when err is null and result is 0", () => { + const result = buildResult(null, 0, () => "out", () => ""); + expect(result).toEqual({ + status: "success", + stdout: "out", + stderr: "", + }); + }); + + it("returns runtime_error when result is non-zero", () => { + const result = buildResult(null, 1, () => "out", () => "err"); + expect(result).toEqual({ + status: "runtime_error", + stdout: "out", + stderr: "err", + }); + }); + + it("returns runtime_error with concatenated stderr when err is thrown", () => { + const err = new Error("fatal"); + const result = buildResult(err, undefined, () => "out", () => "err"); + expect(result.status).toBe("runtime_error"); + expect(result.stdout).toBe("out"); + expect(result.stderr).toContain("err"); + expect(result.stderr).toContain("Error: fatal"); + }); +}); diff --git a/worker/php/package.json b/worker/php/package.json index 406df5c..12127bd 100644 --- a/worker/php/package.json +++ b/worker/php/package.json @@ -5,13 +5,15 @@ "main": "index.mjs", "scripts": { "check": "npm run check:biome", - "check:biome": "biome check --write" + "check:biome": "biome check --write", + "test": "vitest run" }, "dependencies": { "@hono/node-server": "^1.19.9", "hono": "^4.11.9" }, "devDependencies": { - "@biomejs/biome": "^2.3.15" + "@biomejs/biome": "^2.3.15", + "vitest": "^3.2.1" } } diff --git a/worker/php/vitest.config.mjs b/worker/php/vitest.config.mjs new file mode 100644 index 0000000..def6022 --- /dev/null +++ b/worker/php/vitest.config.mjs @@ -0,0 +1,5 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: {}, +}); |
