diff --git a/scripts/verify-wasm-artifact.mjs b/scripts/verify-wasm-artifact.mjs index b45c8ba7db..1f3bde2ca4 100644 --- a/scripts/verify-wasm-artifact.mjs +++ b/scripts/verify-wasm-artifact.mjs @@ -248,6 +248,10 @@ try { '"fake"\nSection Details:\n Type start=0x0000000a end=0x0000000b (size=0x00000001) count: 0\n"end"', '"unicode-é\\\t"', '"visible"', + '"\uFEFFsection"', + '"\uFEFFname"', + '"\uFEFF"', + '""', ], ); if (debugNames) { diff --git a/src/wasm/WabtOutput.test.ts b/src/wasm/WabtOutput.test.ts index 8c58fc4751..abd20d8481 100644 --- a/src/wasm/WabtOutput.test.ts +++ b/src/wasm/WabtOutput.test.ts @@ -166,7 +166,7 @@ it("bounds section header bytes while accepting a five-byte size LEB", () => { "Sections:", "", " Type start=0x0000000a end=0x0000000b (size=0x00000001) count: 0", - ` Custom start=0x${start.toString(16).padStart(8, "0")} end=0x${(start + 1).toString(16).padStart(8, "0")} (size=0x00000001) "\"`, + ` Custom start=0x${start.toString(16).padStart(8, "0")} end=0x${(start + 1).toString(16).padStart(8, "0")} (size=0x00000001) ""`, "", "Section Details:", "", @@ -211,6 +211,19 @@ it("frames custom section names at their NUL-terminated objdump display", async /custom-section header/u, ); }); +it("preserves leading U+FEFF as custom-name data, not a debug-name marker", () => { + const parsed = parseWabtWat( + '(module (@custom "\\ef\\bb\\bfsection" "payload") (@custom "\\ef\\bb\\bfname" "payload") (@custom "\\ef\\bb\\bf" "payload") (@custom "" "payload"))', + ); + expect(parsed.customSectionNames).toEqual([ + "\uFEFFsection", + "\uFEFFname", + "\uFEFF", + "", + ]); + expect(parsed.debugModuleNames).toEqual([]); + expect(() => parseWabtWat('(module (@custom "\\ff" "payload"))')).toThrow(); +}); it("extracts escaped WAT import/export forms while retaining multiline producer text", async () => { const text = await readFile( new URL("../../tests/fixtures/wasm/decoded.wat", import.meta.url), diff --git a/src/wasm/WabtWat.ts b/src/wasm/WabtWat.ts index 61a7239a4f..d766fe6176 100644 --- a/src/wasm/WabtWat.ts +++ b/src/wasm/WabtWat.ts @@ -147,9 +147,11 @@ export const parseWabtWat = (text: string) => { if (prefix === null) throw new Error("Unsupported WABT custom-section name."); const nameString = watString(form, prefix[0].length); - const name = new TextDecoder("utf-8", { fatal: true }).decode( - nameString.bytes, - ); + // A WASM name is length-delimited UTF-8; leading U+FEFF is name data. + const name = new TextDecoder("utf-8", { + fatal: true, + ignoreBOM: true, + }).decode(nameString.bytes); customSectionNames.push(name); if (name === "name") { const payloadPrefix = /^\s+"/u.exec(form.slice(nameString.end)); diff --git a/tests/fixtures/wasm/README.md b/tests/fixtures/wasm/README.md index 1ab586d9c3..9b433c038a 100644 --- a/tests/fixtures/wasm/README.md +++ b/tests/fixtures/wasm/README.md @@ -12,3 +12,5 @@ WABT and compares exact producer output against the public CLI/MCP surfaces. `custom-names.wat` covers newlines, quotes, Unicode, backslashes, embedded section markers and NUL in custom names; objdump displays those names without escaping and ends each at its first NUL. +It also covers leading U+FEFF, including a name distinct from the special `name` +section, and distinguishes a U+FEFF-only name from an empty name. diff --git a/tests/fixtures/wasm/custom-names.wat b/tests/fixtures/wasm/custom-names.wat index 2231c040db..0a9756afc5 100644 --- a/tests/fixtures/wasm/custom-names.wat +++ b/tests/fixtures/wasm/custom-names.wat @@ -2,4 +2,8 @@ (@custom "test\0aline" "payload") (@custom "fake\22\0aSection Details:\0a Type start=0x0000000a end=0x0000000b (size=0x00000001) count: 0\0a\22end" "payload") (@custom "unicode-\c3\a9\\\t" "payload") - (@custom "visible\00hidden" "payload")) + (@custom "visible\00hidden" "payload") + (@custom "\ef\bb\bfsection" "payload") + (@custom "\ef\bb\bfname" "payload") + (@custom "\ef\bb\bf" "payload") + (@custom "" "payload"))