From 7f414d8ea6b495b40f2e162843cd5575e94225e3 Mon Sep 17 00:00:00 2001 From: Carl Walsh Date: Mon, 14 Jul 2025 21:45:17 -0700 Subject: [PATCH 1/2] Fix Unicode in string decoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current code works only for ASCII, but creates garbled bytes for any unicode. Today with e.g. https://www.nytimes.com/crosswords/game/daily/2025/07/13 64 Across should be: "Phrase cooed en español" But with the bug it shows: "Phrase cooed en español" My PUZ file worked fine at https://squares.io/s/jy5a5wyg --- src/puz.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/puz.js b/src/puz.js index 3aa7fdd..6b2c46c 100644 --- a/src/puz.js +++ b/src/puz.js @@ -18,11 +18,8 @@ function strToBytes(str, end) { end = '\0'; } str += end; - var result = new Uint8Array(str.length); - for (var i = 0; i < str.length; i += 1) { - result[i] = str.charCodeAt(i); - } - return result; + const encoder = new TextEncoder(); // UTF-8 by default + return encoder.encode(str); } function concat(byteArrays) { @@ -400,13 +397,14 @@ function PUZtoJSON(buffer) { var ibyte = 52 + ncol * nrow * 2; function readString() { - var result = ""; - var b = bytes[ibyte++]; - while (b !== 0) { - result += String.fromCharCode(b); - b = bytes[ibyte++]; + const start = ibyte; + while (bytes[ibyte] !== 0) { + ibyte++; } - return result; + ibyte++; + + const slice = bytes.slice(start, ibyte - 1); + return new TextDecoder("utf-8").decode(slice); } info.title = readString(); From 1a7f31b62885ad84fd6597cbec29fe6a58d9fe69 Mon Sep 17 00:00:00 2001 From: Carl Walsh Date: Tue, 25 Nov 2025 14:57:31 -0800 Subject: [PATCH 2/2] Sniff the copyright string to determine correct encoding Validation: Correctly parses both: - BrandonKoppyWillShortz-NYTimesSundayJuly132025TunnelVision.puz - DerrickNiedermanWillShortz-NYTimesSundaySeptember72025TheNameIsTheGame.puz --- src/puz.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/puz.js b/src/puz.js index 6b2c46c..2fe6444 100644 --- a/src/puz.js +++ b/src/puz.js @@ -18,7 +18,7 @@ function strToBytes(str, end) { end = '\0'; } str += end; - const encoder = new TextEncoder(); // UTF-8 by default + const encoder = new TextEncoder('ISO-8859-1'); return encoder.encode(str); } @@ -277,7 +277,7 @@ function getExtension(bytes, code) { return null; // could not find } -function getRebus(bytes) { +function getRebus(bytes, encoding) { var grbs = 'GRBS'; var rtbl = 'RTBL'; @@ -286,7 +286,7 @@ function getRebus(bytes) { return; // no rebus } var solbytes = getExtension(bytes, rtbl); - var enc = new TextDecoder('ISO-8859-1'); + var enc = new TextDecoder(encoding); var solstring = enc.decode(new Uint8Array(solbytes)); if (!solstring) { return; @@ -353,7 +353,6 @@ function addRebusToGrid(grid, rebus) { function PUZtoJSON(buffer) { var grid = []; - var info = {}; var across = []; var down = []; var bytes = new Uint8Array(buffer); @@ -395,7 +394,7 @@ function PUZtoJSON(buffer) { } } - var ibyte = 52 + ncol * nrow * 2; + var ibyte; function readString() { const start = ibyte; while (bytes[ibyte] !== 0) { @@ -404,12 +403,25 @@ function PUZtoJSON(buffer) { ibyte++; const slice = bytes.slice(start, ibyte - 1); - return new TextDecoder("utf-8").decode(slice); + return new TextDecoder(encoding).decode(slice); } - info.title = readString(); - info.author = readString(); - info.copyright = readString(); + function readInfo() { + ibyte = 52 + ncol * nrow * 2; + var title = readString(); + var author = readString(); + var copyright = readString(); + return { title, author, copyright }; + } + + // The PUZ spec reqiures ISO-8859-1 not UTF-8, but some files don't follow the spec. + // Sniff the copyright string for the © unicode which is only encoded correctly once. + var encoding = 'utf-8'; + var info = readInfo(); + if (!info.copyright.includes('©')) { + encoding = 'ISO-8859-1'; + info = readInfo(); + } for (var _i2 = 1; _i2 <= n; _i2++) { if (isAcross[_i2]) { @@ -422,7 +434,7 @@ function PUZtoJSON(buffer) { info.description = readString(); - var rebus = getRebus(bytes); + var rebus = getRebus(bytes, encoding); var circles = getCircles(bytes); var shades = getShades(bytes); if (rebus) {