╭ changed ───────────╮ │src/embed/index.html│ ╰────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │ 98 <div id="grid">{{{ HAXY_HTML }}}</div> │ │ 99 <div id="overlay">{{{ HAXY_OVERLAY }}}</div> │ │100 </div> │ │101 - <script id="page-data" type="text/plain">{{{ HAXY_JSON }}}</script> │ │101 + <script id="page-data" type="application/json">{{{ HAXY_JSON }}}</script>│ │102 <script src="/script.js"></script> │ │103 </body> │ │104 </html> │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │1 const grid = document.getElementById("grid"); │ │2 const overlay = document.getElementById("overlay"); │ │3 -const pageJsonBase64 = document.getElementById("page-data").textContent;│ │3 +const pageJson = document.getElementById("page-data").textContent; │ │4 │ │5 let wasmInstance; │ │6 const decoder = new TextDecoder(); │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │159 await document.fonts.ready; │ │160 } │ │161 │ │162 - // the page is embedded as base64 so it can sit inside the host html │ │163 - // without worrying about characters that would terminate the script tag. │ │164 - // decode here and hand the raw json bytes to the wasm. │ │165 - const jsonBytes = Uint8Array.from(atob(pageJsonBase64), (c) => c.charCodeAt(0)); │ │162 + // the page is embedded as raw json in a script tag. │ │163 + // encode to utf-8 bytes and hand it to the wasm. │ │164 + const jsonBytes = encoder.encode(pageJson); │ │165 const ptr = wasmInstance.exports._alloc(jsonBytes.length); │ │166 new Uint8Array(wasmInstance.exports.memory.buffer, ptr, jsonBytes.length).set(jsonBytes);│ │167 wasmInstance.exports._init(ptr, jsonBytes.length, minRows(), maxCols()); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │395 const overlay = try generateOverlay(allocator, &root, &session); │ │396 defer allocator.free(overlay); │ │397 │ │398 - // serialize the snapshot so the wasm side can parse it back without │ │399 - // making a second request. base64-encoded so the json can be embedded │ │400 - // safely inside the host html. │ │398 + // serialize the snapshot so the wasm side can parse it back without making │ │399 + // a second request. it's embedded raw in a <script type="application/json">│ │400 + // block, whose content is raw text terminated only by "</script". in json, │ │401 + // '<' appears solely inside string values, so escaping every '<' (and '>' │ │402 + // for good measure) to its \uXXXX form yields equivalent json that can't │ │403 + // break out of the tag. │ │404 var json: std.Io.Writer.Allocating = .init(allocator); │ │405 defer json.deinit(); │ │406 try std.json.Stringify.value(snapshot, .{}, &json.writer); │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │407 │ │405 - const b64 = std.base64.standard.Encoder; │ │406 - const json_b64 = try allocator.alloc(u8, b64.calcSize(json.written().len));│ │407 - defer allocator.free(json_b64); │ │408 - _ = b64.encode(json_b64, json.written()); │ │408 + var json_escaped: std.ArrayList(u8) = .empty; │ │409 + defer json_escaped.deinit(allocator); │ │410 + for (json.written()) |c| switch (c) { │ │411 + '<' => try json_escaped.appendSlice(allocator, "\\u003c"), │ │412 + '>' => try json_escaped.appendSlice(allocator, "\\u003e"), │ │413 + else => try json_escaped.append(allocator, c), │ │414 + }; │ │415 │ │416 var out: std.ArrayList(u8) = .empty; │ │417 errdefer out.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │420 for (&[_]struct { needle: []const u8, replacement: []const u8 }{ │ │421 .{ .needle = "{{{ HAXY_HTML }}}", .replacement = content }, │ │422 .{ .needle = "{{{ HAXY_OVERLAY }}}", .replacement = overlay }, │ │417 - .{ .needle = "{{{ HAXY_JSON }}}", .replacement = json_b64 }, │ │423 + .{ .needle = "{{{ HAXY_JSON }}}", .replacement = json_escaped.items }, │ │424 }) |sub| { │ │425 const idx = std.mem.indexOfPos(u8, template, cursor, sub.needle) orelse return error.MissingTemplateToken;│ │426 try out.appendSlice(allocator, template[cursor..idx]); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯