aboutsummaryrefslogtreecommitdiffhomepage
path: root/worker/php
diff options
context:
space:
mode:
Diffstat (limited to 'worker/php')
-rw-r--r--worker/php/exec.mjs78
-rw-r--r--worker/php/justfile4
-rw-r--r--worker/php/lib.mjs79
-rw-r--r--worker/php/lib.test.mjs150
-rw-r--r--worker/php/package.json6
-rw-r--r--worker/php/vitest.config.mjs5
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: {},
+});