mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
ui: share links now put the payload in the URL fragment, embed decodes both
This commit is contained in:
1 parent
d7e7b52b3a
commit
108a20fe40
7 files changed
+97
-31
No files matched your search
@@ -38,6 +38,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md).
|
||||
- Click `#include`/`#link` decorations to open the file; offer to import a GitHub repo given by `repo=` ("user/repo" or a tree URL).
|
||||
- Tool info dialog and generated toolchain docs; Help window with ID docs.
|
||||
- File | New only lists extensions that have skeleton files.
|
||||
- "Share Playable Link" packs the game into the URL fragment instead of the query string, so it no longer hits the web server's ~8 KB request-line limit, and encodes it as URL-safe Base64 to avoid percent-encoding. Old `?r=` links still play.
|
||||
- Removed the Markdown/Showdown platform, the old `script` platform, Google Analytics, Sentry (replaced with a new error-reporting endpoint) and the HTTPS redirect.
|
||||
|
||||
### Asset editor
|
||||
|
||||
@@ -357,7 +357,6 @@ body {
|
||||
<p>You can also embed it into an IFRAME:</p>
|
||||
<textarea rows="4" cols="80" id="embedIframeTextarea" class="cliptext"></textarea>
|
||||
<button type="button" class="btn btn-primary" data-clipboard-target="#embedIframeTextarea">Copy IFRAME Tag</button>
|
||||
<p id="embedAdviceWarnIE">Note: These links may be too long for IE/Edge browsers.</p>
|
||||
<p id="embedAdviceWarnAll">Note: These links may be too long for some browsers.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -572,6 +572,58 @@ export function setScriptLoader(loader:ScriptLoader) : ScriptLoader {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// URL-safe Base64 (RFC 4648 §5) helpers, used to pack binary blobs
|
||||
// (e.g. compressed ROMs) into shareable URLs. Unlike standard Base64, the
|
||||
// alphabet ('-' and '_' instead of '+' and '/'/no padding) is safe inside a
|
||||
// URL fragment, so it survives without percent-encoding.
|
||||
const B64URL_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
const B64URL_LOOKUP = (() => {
|
||||
const lut = new Int16Array(128).fill(-1);
|
||||
for (let i = 0; i < B64URL_CHARS.length; ++i) lut[B64URL_CHARS.charCodeAt(i)] = i;
|
||||
// accept standard Base64 too, so old '?r=' links still decode
|
||||
lut[0x2b] = 62; // '+'
|
||||
lut[0x2f] = 63; // '/'
|
||||
return lut;
|
||||
})();
|
||||
|
||||
export function encodeBase64Url(data : Uint8Array) : string {
|
||||
let out = '';
|
||||
for (let i = 0; i < data.length; i += 3) {
|
||||
const b0 = data[i];
|
||||
out += B64URL_CHARS[b0 >> 2];
|
||||
if (i + 1 < data.length) {
|
||||
const b1 = data[i + 1];
|
||||
out += B64URL_CHARS[((b0 & 3) << 4) | (b1 >> 4)];
|
||||
if (i + 2 < data.length) {
|
||||
const b2 = data[i + 2];
|
||||
out += B64URL_CHARS[((b1 & 15) << 2) | (b2 >> 6)];
|
||||
out += B64URL_CHARS[b2 & 63];
|
||||
} else {
|
||||
out += B64URL_CHARS[(b1 & 15) << 2];
|
||||
}
|
||||
} else {
|
||||
out += B64URL_CHARS[(b0 & 3) << 4];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function decodeBase64Url(s : string) : Uint8Array {
|
||||
const out = new Uint8Array((s.length * 3) >> 2);
|
||||
let bits = 0, nbits = 0, o = 0;
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
const v = B64URL_LOOKUP[s.charCodeAt(i)];
|
||||
if (v < 0) continue; // skip padding/whitespace/unknown characters
|
||||
bits = (bits << 6) | v;
|
||||
nbits += 6;
|
||||
if (nbits >= 8) {
|
||||
nbits -= 8;
|
||||
out[o++] = (bits >> nbits) & 0xff;
|
||||
}
|
||||
}
|
||||
return out.subarray(0, o);
|
||||
}
|
||||
|
||||
export function decodeQueryString(qs : string) : {} {
|
||||
if (qs.startsWith('?')) qs = qs.substr(1);
|
||||
var a = qs.split('&');
|
||||
|
||||
+10
-16
@@ -2,7 +2,7 @@
|
||||
import { lzgmini } from "../common/util";
|
||||
import { PLATFORMS } from "../common/emu";
|
||||
import { Platform } from "../common/baseplatform";
|
||||
import { stringToByteArray, getWithBinary, loadScript, getRootBasePlatform } from "../common/util";
|
||||
import { getWithBinary, loadScript, getRootBasePlatform, decodeBase64Url, decodeQueryString } from "../common/util";
|
||||
import { StateRecorderImpl } from "../common/recorder";
|
||||
import { importPlatform } from "../platform/_index";
|
||||
import { saveAs } from "file-saver";
|
||||
@@ -14,19 +14,12 @@ export var stateRecorder : StateRecorderImpl;
|
||||
// external libs (TODO)
|
||||
declare var ga, GIF;
|
||||
|
||||
var _qs = (function (a) {
|
||||
if (!a || a.length == 0)
|
||||
return {};
|
||||
var b = {};
|
||||
for (var i = 0; i < a.length; ++i) {
|
||||
var p = a[i].split('=', 2);
|
||||
if (p.length == 1)
|
||||
b[p[0]] = "";
|
||||
else
|
||||
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
|
||||
}
|
||||
return b;
|
||||
})(window.location.search.substr(1).split('&'));
|
||||
// Payload for the playable-link can be large, so it lives in the URL fragment
|
||||
// (which never reaches the web server); older links may still put it in the
|
||||
// query string, so parse both and let the fragment win.
|
||||
var _qs = decodeQueryString(window.location.search) as { [key: string]: string };
|
||||
var _hashqs = window.location.hash ? decodeQueryString(window.location.hash.substr(1)) as { [key: string]: string } : {};
|
||||
for (var _k in _hashqs) _qs[_k] = _hashqs[_k];
|
||||
|
||||
// catch errors
|
||||
function installErrorHandler() {
|
||||
@@ -155,8 +148,9 @@ async function startPlatform(qs) {
|
||||
}, 'arraybuffer');
|
||||
return true;
|
||||
} else if (lzgvar) {
|
||||
// decompress from lzg
|
||||
var lzgrom = stringToByteArray(atob(lzgvar));
|
||||
// decompress from lzg; the payload is URL-safe Base64 (the decoder also
|
||||
// accepts standard Base64, so pre-fragment links keep working)
|
||||
var lzgrom = decodeBase64Url(lzgvar);
|
||||
rom = new lzgmini().decode(lzgrom);
|
||||
}
|
||||
addPageFocusHandlers();
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export interface GHSession extends GHRepoMetadata {
|
||||
paths?: string[];
|
||||
}
|
||||
|
||||
const README_md_template = "$NAME\n=====\n\n[Open this project in 8bitworkshop](http://8bitworkshop.com/redir.html?platform=$PLATFORM&githubURL=$GITHUBURL&file=$MAINFILE).\n";
|
||||
const README_md_template = "$NAME\n=====\n\n[Open this project in 8bitworkshop](https://8bitworkshop.com/redir.html?platform=$PLATFORM&githubURL=$GITHUBURL&file=$MAINFILE).\n";
|
||||
|
||||
export function getRepos(): { [key: string]: GHRepoMetadata } {
|
||||
var repos = {};
|
||||
|
||||
+10
-12
@@ -1,5 +1,5 @@
|
||||
import { OutputSoundFile, TAPFile } from '../common/audio/CommodoreTape';
|
||||
import { byteArrayToString, compressLZG, getBasePlatform, getFilenameForPath, getFilenamePrefix, loadScript } from '../common/util';
|
||||
import { compressLZG, encodeBase64Url, getBasePlatform, getFilenameForPath, getFilenamePrefix, loadScript } from '../common/util';
|
||||
import { alertError, alertInfo, setWaitDialog, setWaitProgress } from './dialogs';
|
||||
import { getCurrentEditorFilename, getCurrentMainFilename, getCurrentOutput, getCurrentProject, getPlatformStore, getWorkerParams, platform, platform_id, projectWindows } from './ui';
|
||||
import { saveAs } from "file-saver";
|
||||
@@ -20,22 +20,19 @@ export function _shareEmbedLink(e) {
|
||||
// TODO: Module is bad var name (conflicts with MAME)
|
||||
var lzgrom = compressLZG(window['Module'], Array.from(<Uint8Array>getCurrentOutput()));
|
||||
window['Module'] = null; // so we load it again next time
|
||||
var lzgb64 = btoa(byteArrayToString(lzgrom));
|
||||
var embed = {
|
||||
p: platform_id,
|
||||
//n: current_project.mainPath,
|
||||
r: lzgb64
|
||||
};
|
||||
var linkqs = $.param(embed);
|
||||
var fulllink = get8bitworkshopLink(linkqs, 'player.html');
|
||||
// Pack the compressed ROM into the URL fragment so it never reaches the
|
||||
// web server (avoids Apache's ~8190-byte LimitRequestLine), and use
|
||||
// URL-safe Base64 so it needs no percent-encoding.
|
||||
var lzgb64 = encodeBase64Url(lzgrom);
|
||||
var linkqs = $.param({ p: platform_id });
|
||||
var fulllink = get8bitworkshopLink(linkqs, 'player.html', 'r=' + lzgb64);
|
||||
var iframelink = '<iframe width=640 height=600 src="' + fulllink + '">';
|
||||
$("#embedLinkTextarea").text(fulllink);
|
||||
$("#embedIframeTextarea").text(iframelink);
|
||||
$("#embedLinkModal").modal('show');
|
||||
$("#embedAdviceWarnAll").hide();
|
||||
$("#embedAdviceWarnIE").hide();
|
||||
if (fulllink.length >= 65536) $("#embedAdviceWarnAll").show();
|
||||
else if (fulllink.length >= 5120) $("#embedAdviceWarnIE").show();
|
||||
if (fulllink.length >= 65535) $("#embedAdviceWarnAll").show();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -49,12 +46,13 @@ function loadClipboardLibrary() {
|
||||
});
|
||||
}
|
||||
|
||||
function get8bitworkshopLink(linkqs: string, fn: string) {
|
||||
function get8bitworkshopLink(linkqs: string, fn: string, hash?: string) {
|
||||
console.log(linkqs);
|
||||
var loc = window.location;
|
||||
var prefix = loc.pathname.replace('index.html', '');
|
||||
var protocol = (loc.host == '8bitworkshop.com') ? 'https:' : loc.protocol;
|
||||
var fulllink = protocol + '//' + loc.host + prefix + fn + '?' + linkqs;
|
||||
if (hash) fulllink += '#' + hash;
|
||||
return fulllink;
|
||||
}
|
||||
|
||||
|
||||
+23
-1
@@ -2,7 +2,7 @@
|
||||
import assert from "assert";
|
||||
import { describe } from "mocha";
|
||||
import { EmuHalt } from "../../src/common/emu"
|
||||
import { lzgmini, isProbablyBinary, hex } from "../../src/common/util";
|
||||
import { lzgmini, isProbablyBinary, hex, encodeBase64Url, decodeBase64Url } from "../../src/common/util";
|
||||
import { Tokenizer, TokenType } from "../../src/common/tokenizer";
|
||||
import { OPS_6502 } from "../../src/common/cpu/disasm6502";
|
||||
import { MOS6502 } from "../../src/common/cpu/MOS6502";
|
||||
@@ -140,6 +140,28 @@ describe('string functions', function () {
|
||||
// control bytes are a soft signal; one form feed is fine in source
|
||||
assert.ok(!isProbablyBinary(null, new TextEncoder().encode("a\fb\n")));
|
||||
});
|
||||
|
||||
it('Should round-trip URL-safe Base64', function () {
|
||||
for (let len = 0; len <= 64; ++len) {
|
||||
const data = new Uint8Array(len);
|
||||
for (let i = 0; i < len; ++i) data[i] = (i * 37 + len * 11) & 0xff;
|
||||
const enc = encodeBase64Url(data);
|
||||
assert.ok(!/[+/=]/.test(enc), `output must be URL-safe: ${enc}`);
|
||||
assert.deepStrictEqual(Array.from(decodeBase64Url(enc)), Array.from(data));
|
||||
}
|
||||
});
|
||||
|
||||
it('Should encode URL-safe Base64 like standard Base64 (modulo alphabet)', function () {
|
||||
const data = new Uint8Array([0x00, 0x10, 0xff, 0xfe, 0xfb, 0x42, 0x01]);
|
||||
const std = Buffer.from(data).toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
assert.strictEqual(encodeBase64Url(data), std);
|
||||
});
|
||||
|
||||
it('Should decode legacy standard Base64 (old share links)', function () {
|
||||
const data = new Uint8Array([0x01, 0x02, 0x03, 0x80, 0xfe, 0xff]);
|
||||
const std = Buffer.from(data).toString('base64');
|
||||
assert.deepStrictEqual(Array.from(decodeBase64Url(std)), Array.from(data));
|
||||
});
|
||||
});
|
||||
|
||||
describe('EmuHalt', function () {
|
||||
|
||||
Reference in new issue
Block a user