Use session storage instead of location.hash

This commit is contained in:
Evan Hahn 2023-08-02 15:08:17 -05:00
parent 7cf9537f3e
commit 69c66e7d99
8 changed files with 44 additions and 184 deletions

View File

@ -1,38 +0,0 @@
// @ts-check
/**
* Convert a `Uint8Array` to a URL-safe base64 string.
* @param {Uint8Array} bytes
* @returns {string}
*/
export const stringify = (bytes) => {
const string = String.fromCharCode(...bytes);
const base64 = btoa(string);
return base64
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
};
/**
* Convert a URL-safe base64 string to a `Uint8Array`.
* Returns `null` if the string is invalid.
* @param {string} text
* @returns {null | Uint8Array}
*/
export const parse = (text) => {
const normalizedText = text
.replaceAll("-", "+")
.replaceAll("_", "/")
.replaceAll(" ", "+");
try {
const string = atob(normalizedText);
const bytes = new Uint8Array(string.length);
for (let i = 0; i < string.length; i++) {
bytes[i] = string.charCodeAt(i);
}
return bytes;
} catch (_err) {
return null;
}
};

View File

@ -1,41 +0,0 @@
// @ts-check
import maybeJsonParse from "../common/maybeJsonParse.js";
import * as base64url from "../common/base64url.js";
/**
* Parse a location hash into `name` and `bytes`.
* @param {string} hash
* @returns {null | { name: string, bytes: Uint8Array }} The parsed data, or `null` if the hash can't be parsed.
*/
export default (hash) => {
const normalizedHash = decodeURI(hash.replace(/^#/, ""));
const parsed = maybeJsonParse(normalizedHash);
if (!parsed || typeof parsed !== "object") {
console.warn("Couldn't parse hash as JSON object");
return null;
}
if (
!("name" in parsed) || (typeof parsed.name !== "string") ||
!("bytes" in parsed) || (typeof parsed.bytes !== "string")
) {
console.warn("Hash fields missing or invalid type");
return null;
}
const { name } = parsed;
if (!name) {
console.warn("Name is empty");
return null;
}
const bytes = base64url.parse(parsed.bytes);
if (!bytes || !bytes.byteLength) {
console.warn("Bytes is empty");
return null;
}
return { name, bytes };
};

29
public/common/vendor/base64-js.js vendored Normal file
View File

@ -0,0 +1,29 @@
/*! base64-js
*
* The MIT License (MIT)
*
* Copyright (c) 2014 Jameson Little
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// deno-lint-ignore-file
// deno-fmt-ignore-file
var B=Object.create;var l=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,j=Object.prototype.hasOwnProperty;var H=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),U=(r,e)=>{for(var t in e)l(r,t,{get:e[t],enumerable:!0})},A=(r,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of k(e))!j.call(r,o)&&o!==t&&l(r,o,{get:()=>e[o],enumerable:!(a=_(e,o))||a.enumerable});return r},u=(r,e,t)=>(A(r,e,"default"),t&&A(t,e,"default")),C=(r,e,t)=>(t=r!=null?B(w(r)):{},A(e||!r||!r.__esModule?l(t,"default",{value:r,enumerable:!0}):t,r));var p=H(y=>{"use strict";y.byteLength=I;y.toByteArray=T;y.fromByteArray=D;var h=[],d=[],E=typeof Uint8Array<"u"?Uint8Array:Array,s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";for(F=0,L=s.length;F<L;++F)h[F]=s[F],d[s.charCodeAt(F)]=F;var F,L;d["-".charCodeAt(0)]=62;d["_".charCodeAt(0)]=63;function g(r){var e=r.length;if(e%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var t=r.indexOf("=");t===-1&&(t=e);var a=t===e?0:4-t%4;return[t,a]}function I(r){var e=g(r),t=e[0],a=e[1];return(t+a)*3/4-a}function O(r,e,t){return(e+t)*3/4-t}function T(r){var e,t=g(r),a=t[0],o=t[1],n=new E(O(r,a,o)),v=0,x=o>0?a-4:a,f;for(f=0;f<x;f+=4)e=d[r.charCodeAt(f)]<<18|d[r.charCodeAt(f+1)]<<12|d[r.charCodeAt(f+2)]<<6|d[r.charCodeAt(f+3)],n[v++]=e>>16&255,n[v++]=e>>8&255,n[v++]=e&255;return o===2&&(e=d[r.charCodeAt(f)]<<2|d[r.charCodeAt(f+1)]>>4,n[v++]=e&255),o===1&&(e=d[r.charCodeAt(f)]<<10|d[r.charCodeAt(f+1)]<<4|d[r.charCodeAt(f+2)]>>2,n[v++]=e>>8&255,n[v++]=e&255),n}function q(r){return h[r>>18&63]+h[r>>12&63]+h[r>>6&63]+h[r&63]}function z(r,e,t){for(var a,o=[],n=e;n<t;n+=3)a=(r[n]<<16&16711680)+(r[n+1]<<8&65280)+(r[n+2]&255),o.push(q(a));return o.join("")}function D(r){for(var e,t=r.length,a=t%3,o=[],n=16383,v=0,x=t-a;v<x;v+=n)o.push(z(r,v,v+n>x?x:v+n));return a===1?(e=r[t-1],o.push(h[e>>2]+h[e<<4&63]+"==")):a===2&&(e=(r[t-2]<<8)+r[t-1],o.push(h[e>>10]+h[e>>4&63]+h[e<<2&63]+"=")),o.join("")}});var c={};U(c,{byteLength:()=>G,default:()=>N,fromByteArray:()=>K,toByteArray:()=>J});var i=C(p());u(c,C(p()));var{byteLength:G,toByteArray:J,fromByteArray:K}=i,{default:m,...M}=i,N=m!==void 0?m:M;export{G as byteLength,N as default,K as fromByteArray,J as toByteArray};

View File

@ -1,17 +1,16 @@
// @ts-check
import parseHash from "../common/parseHash.js";
import * as base64 from "../common/vendor/base64-js.js";
const main = () => {
// TODO: We may want a better UI here.
// TODO: Handle hash changes.
const parsedHash = parseHash(location.hash);
if (!parsedHash) {
// TODO: Handle errors.
const fileDataBase64 = window.sessionStorage.getItem("fileData");
if (!fileDataBase64) {
location.href = "..";
return;
}
const { bytes } = parsedHash;
const bytes = base64.toByteArray(fileDataBase64);
console.log(bytes);
};

View File

@ -1,8 +1,8 @@
// @ts-check
import * as base64 from "../common/vendor/base64-js.js";
import { SUPPORTED_FILE_TYPES } from "./constants.js";
import crel from "../common/crel.js";
import * as base64url from "../common/base64url.js";
import routeFile from "./routeFile.js";
const accept = SUPPORTED_FILE_TYPES
@ -51,16 +51,6 @@ const disclaimerParagraphEl = crel(
crel("small", {}, "Your files do not leave your computer."),
);
/**
* @param {Blob} blob
* @returns {Promise<string>}
*/
const formatBytes = async (blob) => {
const arrayBuffer = await blob.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
return base64url.stringify(bytes);
};
const main = () => {
const appEl = document.getElementById("app");
if (!appEl) throw new Error("HTML is not set up correctly");
@ -81,10 +71,10 @@ const main = () => {
return;
}
location.href = route + "#" + JSON.stringify({
name: file.name,
bytes: await formatBytes(file),
});
const fileData = new Uint8Array(await file.arrayBuffer());
window.sessionStorage.setItem("fileData", base64.fromByteArray(fileData));
location.href = route;
});
appEl.append(labelParagraphEl, inputContainerEl, disclaimerParagraphEl);

View File

@ -1,6 +1,6 @@
// @ts-check
import parseHash from "../common/parseHash.js";
import * as base64 from "../common/vendor/base64-js.js";
import parsePng from "./parsePng.js";
import getNodeUi from "./getNodeUi.js";
import explorer from "./explorer.js";
@ -11,14 +11,13 @@ if (!errorEl || !explorerEl) throw new Error("HTML is not set up correctly");
const main = () => {
// TODO: We may want a better UI here.
// TODO: Handle hash changes.
const parsedHash = parseHash(location.hash);
if (!parsedHash) {
// TODO: Handle errors.
const fileDataBase64 = window.sessionStorage.getItem("fileData");
if (!fileDataBase64) {
location.href = "..";
return;
}
const { bytes } = parsedHash;
const bytes = base64.toByteArray(fileDataBase64);
const rootNode = parsePng(bytes);
if (!rootNode) {

View File

@ -1,43 +0,0 @@
import { assertEquals } from "assert";
import { b } from "../helpers.ts";
import * as base64url from "../../public/common/base64url.js";
Deno.test('encodes no bytes as ""', () => {
assertEquals(base64url.stringify(b()), "");
});
Deno.test("encodes in a URL-safe way", () => {
const input = b(105, 183, 62, 249, 215, 191, 254);
assertEquals(
base64url.stringify(input),
"abc--de__g",
);
});
Deno.test("decodes the empty string", () => {
assertEquals(base64url.parse(""), b());
});
Deno.test("decodes URL-safe strings", () => {
assertEquals(base64url.parse("_-o"), b(255, 234));
});
Deno.test("decodes URL-unsafe strings (as a bonus)", () => {
assertEquals(base64url.parse("/+o="), b(255, 234));
});
Deno.test("round-trips", () => {
const testCases = [
b(),
b(1),
b(1, 2, 3),
b(255, 234),
b(105, 183, 62, 249, 215, 191, 254),
];
for (const original of testCases) {
const string = base64url.stringify(original);
const parsed = base64url.parse(string);
assertEquals(parsed, original, `Round-trip failed for ${original}`);
}
});

View File

@ -1,35 +0,0 @@
import { assertEquals } from "assert";
import { stubbingWarn } from "../helpers.ts";
import parseHash from "../../public/common/parseHash.js";
Deno.test(
"returns null if hash cannot be parsed",
stubbingWarn(() => {
const testCases = [
// Missing fields
"#null",
"#{}",
"#{%22name%22:%22image.png%22}",
"#{%22bytes%22:%22AQID%22}",
// Invalid JSON
"",
"#{%22name%22:%22small.png%22,%22bytes%22:%22iAQID",
];
for (const testCase of testCases) {
assertEquals(
parseHash(testCase),
null,
`Parsing ${testCase} should fail`,
);
}
}),
);
Deno.test("parses hashes", () => {
const hash = "#{%22name%22:%22small.png%22,%22bytes%22:%22AQID%22}";
assertEquals(parseHash(hash), {
name: "small.png",
bytes: new Uint8Array([1, 2, 3]),
});
});