diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb7e62067..a8dc167e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -493,7 +493,7 @@ jobs: tests/acceptance/webSourceLocationCli.test.ts tests/boundary/cli/referenceSourceImportStatus.test.ts tests/boundary/filesystem/asarInventoryFreshness.test.ts - tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts + tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts tests/conformance/providers/ghidra/windowsCapabilities.test.ts src/ghidra/GhidraInstallation.test.ts tests/boundary/providers/ghidra/ghidraTransport.test.ts diff --git a/docs/testing.md b/docs/testing.md index 3c0d401b7..45c3dcf84 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1386,7 +1386,8 @@ generation. It installs nothing. Without configuration, the default invocation reports a named skip; `--require-tools` makes missing configuration fail. The verifier creates and validates real modules with custom sections, multiple -import/export kinds, escaped names and empty modules. It compares exact WAT, +import/export kinds, escaped names, an empty module, and a bulk-memory +DataCount section. It compares exact WAT, section output and selected/tool digests through the compiled CLI and real MCP SDK, validates advertised schemas, checks identical CLI/MCP Evidence IDs and session bundle readback, retains invalid/truncated module diagnostics, preserves diff --git a/scripts/verify-wasm-artifact.mjs b/scripts/verify-wasm-artifact.mjs index ce139c5e2..9a8c53644 100644 --- a/scripts/verify-wasm-artifact.mjs +++ b/scripts/verify-wasm-artifact.mjs @@ -122,6 +122,15 @@ try { new URL("../tests/fixtures/wasm/custom-names.wat", import.meta.url), "utf8", ), + `(module + (memory 1) + (data "x") + (func (export "init") + i32.const 0 + i32.const 0 + i32.const 1 + memory.init 0 + data.drop 0))\n`, ]; for (const [index, source] of sources.entries()) { const fixtureRoot = join(root.path, `fixture-${index}`); @@ -220,6 +229,22 @@ try { '"unicode-é\\\t"', ], ); + if (index === 4) { + assert.match( + headers, + /^DataCount start=0x[\da-f]+ end=0x[\da-f]+ \(size=0x[\da-f]+\) count: 1$/m, + ); + assert.deepEqual( + report.sections.find(({ kind }) => kind === "DataCount"), + { + kind: "DataCount", + start: 0x23, + end: 0x24, + bytes: 1, + description: "count: 1", + }, + ); + } const bundleReply = await client.callTool({ name: "get_evidence_bundle", arguments: {}, diff --git a/src/wasm/WabtOutput.test.ts b/src/wasm/WabtOutput.test.ts index b593a88df..c12402b38 100644 --- a/src/wasm/WabtOutput.test.ts +++ b/src/wasm/WabtOutput.test.ts @@ -33,6 +33,110 @@ it("retains real WABT section ranges, custom section identity and original heade ["test"], ), ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + text.replace( + "\nSection Details:\n", + "\nDataCount start=0x00000040 end=0x0000004c (size=0x0000000c)\nSection Details:\n", + ), + 76, + ["test"], + ), + ).toThrow(/Unrecognized/u); +}); +const dataCountModuleBytes = 0x3d; +const unpaddedDataCountRow = + "DataCount start=0x00000023 end=0x00000024 (size=0x00000001) count: 1"; +const dataCountDump = [ + "", + "module.wasm:\tfile format wasm 0x1", + "", + "Sections:", + "", + " Type start=0x0000000a end=0x0000000e (size=0x00000004) count: 1", + " Function start=0x00000010 end=0x00000012 (size=0x00000002) count: 1", + " Memory start=0x00000014 end=0x00000017 (size=0x00000003) count: 1", + " Export start=0x00000019 end=0x00000021 (size=0x00000008) count: 1", + unpaddedDataCountRow, + " Code start=0x00000026 end=0x00000037 (size=0x00000011) count: 1", + " Data start=0x00000039 end=0x0000003d (size=0x00000004) count: 1", + "", + "Section Details:", + "", +].join("\n"); +it("accepts an unpadded DataCount row and still rejects malformed output and bad ranges", () => { + const parsed = parseWabtOutput(dataCountDump, dataCountModuleBytes, []); + expect(parsed.sections.map(({ kind }) => kind)).toEqual([ + "Type", + "Function", + "Memory", + "Export", + "DataCount", + "Code", + "Data", + ]); + expect(parsed.sections.find(({ kind }) => kind === "DataCount")).toEqual({ + kind: "DataCount", + start: 0x23, + end: 0x24, + bytes: 1, + description: "count: 1", + }); + expect(parsed.headers).toContain(`\n${unpaddedDataCountRow}\n`); + expect(() => + parseWabtOutput( + dataCountDump.replace(unpaddedDataCountRow, "DataCount count: 1"), + dataCountModuleBytes, + [], + ), + ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + dataCountDump.replace(unpaddedDataCountRow, "Sections:"), + dataCountModuleBytes, + [], + ), + ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + dataCountDump.replace( + unpaddedDataCountRow, + "DataCount start=0x00000023 end=0x00000024 (size=0x00000001)count: 1", + ), + dataCountModuleBytes, + [], + ), + ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + dataCountDump.replace("(size=0x00000001)", "(size=0x00000002)"), + dataCountModuleBytes, + [], + ), + ).toThrow(/ranges/u); + expect(() => + parseWabtOutput( + dataCountDump.replace( + "DataCount start=0x00000023 end=0x00000024 (size=0x00000001)", + "DataCount start=0x00000020 end=0x00000021 (size=0x00000001)", + ), + dataCountModuleBytes, + [], + ), + ).toThrow(/ranges/u); + expect(() => + parseWabtOutput(dataCountDump, dataCountModuleBytes - 1, []), + ).toThrow(/ranges/u); + expect(() => + parseWabtOutput( + dataCountDump.replace( + "\nmodule.wasm:\tfile format wasm 0x1\n\nSections:\n\n", + "", + ), + dataCountModuleBytes, + [], + ), + ).toThrow(/header/u); }); it("extracts escaped WAT import/export forms while retaining multiline producer text", async () => { const text = await readFile( diff --git a/src/wasm/WabtOutput.ts b/src/wasm/WabtOutput.ts index f2406dbdf..a434b7d17 100644 --- a/src/wasm/WabtOutput.ts +++ b/src/wasm/WabtOutput.ts @@ -13,7 +13,7 @@ export const parseWabtOutput = ( const sections = []; while (!text.startsWith("\nSection Details:\n", offset)) { const match = - /^[ \t]+(\w+) start=0x([\da-f]+) end=0x([\da-f]+) \(size=0x([\da-f]+)\) /u.exec( + /^[ \t]*(\w+) start=0x([\da-f]+) end=0x([\da-f]+) \(size=0x([\da-f]+)\) /u.exec( text.slice(offset), ); if (match === null) throw new Error("Unrecognized WABT section row."); diff --git a/tests/boundary/filesystem/mipsAbiFlags.test.ts b/tests/boundary/filesystem/mipsAbiFlags.test.ts index 1c5467d0a..4cab9b702 100644 --- a/tests/boundary/filesystem/mipsAbiFlags.test.ts +++ b/tests/boundary/filesystem/mipsAbiFlags.test.ts @@ -131,7 +131,7 @@ for (const little of [true, false]) { const fp64 = await inspect(bytes, little); assert.ok(fp64.ok); const changed = target({ ...metadata(little), abiFlags: fp64.value }); - assert.match(ghidraMipsUnsupportedReason(changed) ?? "", /register modes/u); + assert.match(ghidraMipsUnsupportedReason(changed) ?? "", /FP64/u); assert.notDeepEqual(ghidraMipsProfileParameters(changed), before); }); } @@ -271,14 +271,6 @@ const unsupported: [string, (bytes: Buffer) => void, RegExp][] = [ }, /MIPS32r2/u, ], - [ - "soft-float", - (b) => { - b[abiOffset + 5] = 0; - b[abiOffset + 7] = 3; - }, - /register modes/u, - ], [ "unspecified FP ABI", (b) => { @@ -309,6 +301,19 @@ for (const [name, change, reason] of unsupported) { ); }); } +it("resolves soft-float ABI metadata and commits its integer-register storage policy", async () => { + const bytes = fixture(); + bytes[abiOffset + 5] = 0; + bytes[abiOffset + 7] = 3; + const resolved = await resolveFixture(bytes); + assert.equal(resolved.mips.abiFlags?.cpr1Size, 0); + assert.equal(resolved.mips.abiFlags?.fpAbi, 3); + assert.equal(ghidraMipsUnsupportedReason(resolved), null); + assert.equal( + ghidraMipsProfileParameters(resolved).mips_soft_float_storage, + "o32-soft-float-prototype-v1", + ); +}); it("preserves the caller's cancellation error during table inspection", async () => { const cancelled = new Error("caller cancellation sentinel"); let checks = 0;