shithub: puzzles

Download patch

ref: f729f51e475ff98d0caf529f0723ef810b1c88ef
parent: 1c760b2ee808ba68781a68a57292cc841b3df5a0
author: Simon Tatham <anakin@pobox.com>
date: Sun May 23 04:45:55 EDT 2021

WASM: move save file encoding from JS into C.

The previous fix worked OK, but it was conceptually wrong. Puzzles
save files are better regarded as binary, not text: the length fields
are measured in bytes, so translating the file into a different
multibyte character encoding would invalidate them.

So it was wrong to fetch a C byte string containing the exactly right
binary data, then translate it into a Javascript string as if decoding
from UTF-8, then retranslate to a representation of a bytewise
encoding via encodeURIComponent, and then label the result as
application/octet-stream.

This probably wouldn't have caused any problems in practice, because I
don't remember any situation in which my save files use characters
outside printable ASCII (plus newline). But it's not actually
forbidden, so a save file might choose to do that some day, so that
UTF-8 decode/reencode hidden in the JS was a latent bug.

Now the URI-encoding is done on the C side, while we still know
exactly what the binary data ought to look like and can be sure we're
translating it byte for byte into the output encoding for the data:
URI. By the time the JS receives a string pointer from get_save_file,
it's already URI-encoded, which _guarantees_ that it's in ASCII and
won't be messed about with by Emscripten's UTF8ToString.

--- a/emcc.c
+++ b/emcc.c
@@ -787,12 +787,53 @@
     size_t pos;
 };
 
-static void savefile_write(void *vctx, const void *buf, int len)
+static void savefile_write(void *vctx, const void *vbuf, int len)
 {
+    static const unsigned char length[256] = {
+        /*
+         * Assign a length of 1 to any printable ASCII character that
+         * can be written literally in URI-encoding, i.e.
+         *
+         *    A-Z a-z 0-9 - _ . ! ~ * ' ( )
+         *
+         * Assign length 3 (for % and two hex digits) to all other
+         * byte values.
+         */
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 1, 3, 3, 3, 3, 3, 1, 1, 1, 1, 3, 3, 1, 1, 3,
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3,
+        3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 1,
+        3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+    };
+    static const char hexdigits[] = "0123456789ABCDEF";
+
     struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)vctx;
-    if (ctx->buffer)
-        memcpy(ctx->buffer + ctx->pos, buf, len);
-    ctx->pos += len;
+    const unsigned char *buf = (const unsigned char *)vbuf;
+    for (int i = 0; i < len; i++) {
+        unsigned char c = buf[i];
+        int clen = length[c];
+        if (ctx->buffer) {
+            if (clen == 1) {
+                ctx->buffer[ctx->pos] = c;
+            } else {
+                ctx->buffer[ctx->pos] = '%';
+                ctx->buffer[ctx->pos+1] = hexdigits[c >> 4];
+                ctx->buffer[ctx->pos+2] = hexdigits[c & 0xF];
+            }
+        }
+        ctx->pos += clen;
+    }
 }
 
 char *get_save_file(void)
--- a/emccpre.js
+++ b/emccpre.js
@@ -379,8 +379,7 @@
                 "Click to download the "));
             var a = document.createElement("a");
             a.download = "puzzle.sav";
-            a.href = "data:application/octet-stream," +
-                encodeURIComponent(savefile_text);
+            a.href = "data:application/octet-stream," + savefile_text;
             a.appendChild(document.createTextNode("saved-game file"));
             dlg_form.appendChild(a);
             dlg_form.appendChild(document.createTextNode("."));