diff --git a/src/core/postscript/wasm_compiler.js b/src/core/postscript/wasm_compiler.js index 1b552c3b8..bdc7b555f 100644 --- a/src/core/postscript/wasm_compiler.js +++ b/src/core/postscript/wasm_compiler.js @@ -283,6 +283,21 @@ class PsWasmCompiler { } while (n !== 0); } + // `i32.const` immediates are signed LEB128 (Wasm spec), so they must be + // emitted with sign extension β€” the unsigned encoder mis-encodes any value + // whose final 7-bit group has bit 0x40 set (e.g. 64 β†’ 0x40 β†’ decoded as βˆ’64). + _emitSLEB128(n) { + for (;;) { + const b = n & 0x7f; + n >>= 7; // arithmetic shift keeps the sign bit + if ((n === 0 && (b & 0x40) === 0) || (n === -1 && (b & 0x40) !== 0)) { + this._code.push(b); + return; + } + this._code.push(b | 0x80); + } + } + _emitF64Const(value) { this._code.push(OP.f64_const); PsWasmCompiler.#f64View.setFloat64(0, value, true /* little-endian */); @@ -532,11 +547,11 @@ class PsWasmCompiler { const shift = first.value; if (shift > 0) { code.push(OP.i32_const); - this._emitULEB128(shift); + this._emitSLEB128(shift); code.push(OP.i32_shl); } else if (shift < 0) { code.push(OP.i32_const); - this._emitULEB128(-shift); + this._emitSLEB128(-shift); code.push(OP.i32_shr_s); } code.push(OP.f64_convert_i32_s); @@ -870,7 +885,7 @@ class PsWasmCompiler { const min = this._range[i * 2]; const max = this._range[i * 2 + 1]; code.push(OP.i32_const); - this._emitULEB128(i * 8); + this._emitSLEB128(i * 8); if (!this._compileNode(outputs[i])) { return null; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index e25516a81..2d5724f6e 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -1,6 +1,7 @@ *.pdf *.error +!postscript_type4_many_outputs.pdf !boundingBox_invalid.pdf !pdkids.pdf !tracemonkey.pdf diff --git a/test/pdfs/postscript_type4_many_outputs.pdf b/test/pdfs/postscript_type4_many_outputs.pdf new file mode 100644 index 000000000..23d775b44 --- /dev/null +++ b/test/pdfs/postscript_type4_many_outputs.pdf @@ -0,0 +1,51 @@ +%PDF-1.7 +%βγΟΣ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Shading << /Sh1 4 0 R >> >> /Contents 5 0 R >> +endobj +4 0 obj +<< /ShadingType 2 /ColorSpace 6 0 R /Coords [0 0 200 0] /Domain [0 1] /Function 7 0 R /Extend [true true] >> +endobj +5 0 obj +<< /Length 30 >> +stream +q 0 0 200 200 re W n /Sh1 sh Q +endstream +endobj +6 0 obj +[/DeviceN [/c0 /c1 /c2 /c3 /c4 /c5 /c6 /c7 /c8] /DeviceCMYK 8 0 R] +endobj +7 0 obj +<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1] /Length 35 >> +stream +{ dup dup dup dup dup dup dup dup } +endstream +endobj +8 0 obj +<< /FunctionType 4 /Domain [0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1] /Range [0 1 0 1 0 1 0 1] /Length 41 >> +stream +{ pop pop pop pop pop pop pop pop 0 0 0 } +endstream +endobj +xref +0 9 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +0000000251 00000 n +0000000375 00000 n +0000000455 00000 n +0000000537 00000 n +0000000697 00000 n +trailer +<< /Size 9 /Root 1 0 R >> +startxref +875 +%%EOF \ No newline at end of file diff --git a/test/test_manifest.json b/test/test_manifest.json index 361187d51..cad98ab14 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1,4 +1,11 @@ [ + { + "id": "postscript-type4-many-outputs", + "file": "pdfs/postscript_type4_many_outputs.pdf", + "md5": "90c1ebd35acc6c7d01f4fdb0ae0bef82", + "rounds": 1, + "type": "eq" + }, { "id": "bug1755201", "file": "pdfs/bug1755201.pdf", diff --git a/test/unit/postscript_spec.js b/test/unit/postscript_spec.js index cdfdcc0b5..b58c277ba 100644 --- a/test/unit/postscript_spec.js +++ b/test/unit/postscript_spec.js @@ -1049,6 +1049,26 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () { ); expect(r2).toBeCloseTo(0.5, 9); }); + + it("compiles functions with 9+ outputs (signed i32.const store offset)", async function () { + // Regression: each output's f64.store address is emitted as + // `i32.const (i * 8)`. i32.const immediates are *signed* LEB128, so the + // 9th output offset (64) must not be written with the unsigned encoder, + // which yields the byte 0x40 that Wasm decodes as -64 β†’ out-of-bounds + // store β†’ runtime trap. compileAndRun throws if the Wasm function traps. + for (const nOut of [9, 10, 16, 20]) { + const range = []; + for (let i = 0; i < nOut; i++) { + range.push(0, 1000); + } + const src = "{" + " dup".repeat(nOut - 1) + " }"; + const out = compileAndRun(src, [0, 1], range, [0.5]); + expect(out.length).toBe(nOut); + for (const value of out) { + expect(value).toBeCloseTo(0.5, 10); + } + } + }); }); // PSStackToTree