diff --git a/packages/xd-crossword-tools/src/vendor/puzjs.ts b/packages/xd-crossword-tools/src/vendor/puzjs.ts index b3e74f6..80a3726 100644 --- a/packages/xd-crossword-tools/src/vendor/puzjs.ts +++ b/packages/xd-crossword-tools/src/vendor/puzjs.ts @@ -458,9 +458,21 @@ function PUZtoJSON(buffer: ArrayBuffer) { var ncol = bytes[44] var nrow = bytes[45] + if (!ncol || !nrow) { + throw new Error(`Invalid PUZ file: header reports grid size ${ncol}x${nrow}`) + } if (!(bytes[50] === 0 && bytes[51] === 0)) { throw new Error("Scrambled PUZ file") } + // Header must be followed by solution (ncol*nrow) and progress (ncol*nrow) bytes + // before the NUL-terminated string section starts. If the file is shorter than + // that the puzzle is truncated and we cannot safely decode it. + var minBytes = 52 + 2 * ncol * nrow + if (bytes.length < minBytes) { + throw new Error( + `Invalid PUZ file: header reports ${ncol}x${nrow} grid (needs at least ${minBytes} bytes) but file is only ${bytes.length} bytes` + ) + } for (var i = 0; i < nrow; i++) { grid[i] = [] @@ -475,8 +487,8 @@ function PUZtoJSON(buffer: ArrayBuffer) { return i < 0 || j < 0 || i >= nrow || j >= ncol || grid[i][j] === "." } - var isAcross = [] - var isDown = [] + var isAcross: boolean[] = [] + var isDown: boolean[] = [] var n = 0 for (var _i = 0; _i < nrow; _i++) { for (var _j = 0; _j < ncol; _j++) { @@ -496,8 +508,18 @@ function PUZtoJSON(buffer: ArrayBuffer) { var ibyte = 52 + ncol * nrow * 2 function readString() { var result = "" + if (ibyte >= bytes.length) { + throw new Error( + `Invalid PUZ file: reached end of file while reading strings. The grid scan found ${n} numbered cells (${ + isAcross.filter(Boolean).length + } across, ${isDown.filter(Boolean).length} down) which doesn't match the strings stored in the file — the puzzle is likely truncated or its header grid size (${ncol}x${nrow}) is wrong.` + ) + } var b = bytes[ibyte++] while (b !== 0) { + if (b === undefined) { + throw new Error("Invalid PUZ file: unterminated string at end of file") + } result += String.fromCharCode(b) b = bytes[ibyte++] } diff --git a/packages/xd-crossword-tools/tests/puz/truncated-strings.puz b/packages/xd-crossword-tools/tests/puz/truncated-strings.puz new file mode 100644 index 0000000..3279cc7 Binary files /dev/null and b/packages/xd-crossword-tools/tests/puz/truncated-strings.puz differ diff --git a/packages/xd-crossword-tools/tests/puz2XD.test.ts b/packages/xd-crossword-tools/tests/puz2XD.test.ts index 9f38500..339e8e5 100644 --- a/packages/xd-crossword-tools/tests/puz2XD.test.ts +++ b/packages/xd-crossword-tools/tests/puz2XD.test.ts @@ -173,3 +173,9 @@ it("handles greyd backgrounds", () => { `) // expect(xd).toMatchFile(`./tests/output/${file}.xd`) }) + +it("throws a clear error for truncated/malformed puz files instead of looping forever", () => { + const path = __dirname + "/puz/truncated-strings.puz" + const puz = readFileSync(path) + expect(() => puzToXD(puz)).toThrowError(/Invalid PUZ file/) +})