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("."));