diff --git a/typescript/package.json b/typescript/package.json index c283643e..e559e7ea 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -21,7 +21,7 @@ }, "scripts": { "build": "tsc -p src", - "test": "npm run build && tsc -p test && node --test out/validate.test.js out/zod.test.js tests/model.test.mjs", + "test": "npm run build && tsc -p test && node --test out/validate.test.js out/zod.test.js out/program.test.js tests/model.test.mjs", "build-all": "npm run build --workspaces", "prepare": "npm run build-all", "prepublishOnly": "node -e \"require('fs').copyFileSync('../SECURITY.md','SECURITY.md')\"", diff --git a/typescript/src/ts/program.ts b/typescript/src/ts/program.ts index 0621ed40..99231463 100644 --- a/typescript/src/ts/program.ts +++ b/typescript/src/ts/program.ts @@ -108,7 +108,7 @@ export function createModuleTextFromProgram(jsonObject: object): Result function objectToString(obj: Record) { if (obj.hasOwnProperty("@ref")) { const index = obj["@ref"]; - if (typeof index === "number" && index < currentStep && Object.keys(obj).length === 1) { + if (typeof index === "number" && Number.isInteger(index) && index >= 0 && index < currentStep && Object.keys(obj).length === 1) { return `step${index + 1}`; } } @@ -160,9 +160,10 @@ export async function evaluateJsonProgram(program: Program, onCall: (func: strin async function evaluateObject(obj: Record) { if (obj.hasOwnProperty("@ref")) { const index = obj["@ref"]; - if (typeof index === "number" && index < results.length) { + if (Object.keys(obj).length === 1 && typeof index === "number" && Number.isInteger(index) && index >= 0 && index < results.length) { return results[index]; } + throw new Error(`Invalid result reference: ${String(index)}`); } else if (obj.hasOwnProperty("@func")) { const func = obj["@func"]; diff --git a/typescript/test/program.test.ts b/typescript/test/program.test.ts new file mode 100644 index 00000000..f6a53156 --- /dev/null +++ b/typescript/test/program.test.ts @@ -0,0 +1,106 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { evaluateJsonProgram, createModuleTextFromProgram, Program } from "../dist/ts/index.js"; + +// --------------------------------------------------------------------------- +// evaluateJsonProgram result-reference bounds checking +// --------------------------------------------------------------------------- + +describe("evaluateJsonProgram result references", () => { + async function run(program: Program) { + const calls: Array<{ func: string; args: unknown[] }> = []; + const result = await evaluateJsonProgram(program, async (func, args) => { + calls.push({ func, args }); + return { func, args }; + }); + return { result, calls }; + } + + it("resolves a valid reference to a preceding step", async () => { + const program: Program = { + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": 0 }] }, + ], + }; + const { calls } = await run(program); + assert.deepEqual(calls[1].args, [{ func: "first", args: [] }]); + }); + + it("throws on a negative reference instead of yielding undefined", async () => { + const program: Program = { + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": -1 }] }, + ], + }; + await assert.rejects(run(program), /Invalid result reference/); + }); + + it("throws on a non-integer reference", async () => { + const program: Program = { + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": 0.5 }] }, + ], + }; + await assert.rejects(run(program), /Invalid result reference/); + }); + + it("throws on an out-of-upper-bound reference", async () => { + const program: Program = { + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": 5 }] }, + ], + }; + await assert.rejects(run(program), /Invalid result reference/); + }); +}); + +// --------------------------------------------------------------------------- +// createModuleTextFromProgram result-reference bounds checking +// --------------------------------------------------------------------------- + +describe("createModuleTextFromProgram result references", () => { + it("emits a step reference for a valid index", () => { + const result = createModuleTextFromProgram({ + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": 0 }] }, + ], + }); + assert.equal(result.success, true); + assert.match((result as { data: string }).data, /step1/); + }); + + it("rejects a negative index", () => { + const result = createModuleTextFromProgram({ + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": -1 }] }, + ], + }); + assert.equal(result.success, false); + }); + + it("rejects a non-integer index", () => { + const result = createModuleTextFromProgram({ + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": 0.5 }] }, + ], + }); + assert.equal(result.success, false); + }); + + it("rejects an out-of-upper-bound index", () => { + const result = createModuleTextFromProgram({ + "@steps": [ + { "@func": "first" }, + { "@func": "second", "@args": [{ "@ref": 5 }] }, + ], + }); + assert.equal(result.success, false); + }); +});