PNG: basic skeleton
This commit is contained in:
parent
8eed5b60da
commit
79498239c4
|
@ -4,7 +4,8 @@
|
|||
"check-all": "deno fmt --check && deno lint && deno task typecheck && deno test --allow-read"
|
||||
},
|
||||
"imports": {
|
||||
"assert": "https://deno.land/std@0.196.0/testing/asserts.ts"
|
||||
"assert": "https://deno.land/std@0.196.0/testing/asserts.ts",
|
||||
"mock": "https://deno.land/std@0.196.0/testing/mock.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.ns", "dom"]
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"https://deno.land/std@0.196.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a",
|
||||
"https://deno.land/std@0.196.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536",
|
||||
"https://deno.land/std@0.196.0/fmt/colors.ts": "a7eecffdf3d1d54db890723b303847b6e0a1ab4b528ba6958b8f2e754cf1b3bc",
|
||||
"https://deno.land/std@0.196.0/testing/asserts.ts": "b4e4b1359393aeff09e853e27901a982c685cb630df30426ed75496961931946"
|
||||
"https://deno.land/std@0.196.0/testing/asserts.ts": "b4e4b1359393aeff09e853e27901a982c685cb630df30426ed75496961931946",
|
||||
"https://deno.land/std@0.196.0/testing/mock.ts": "4c52b8312d159179fdd9d9a1b35e342ee4e1a1248f29e5c7f57fb4011c3f55ed"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
// @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;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
// @ts-check
|
||||
|
||||
/**
|
||||
* Like `JSON.parse`, but returns `null` instead of throwing an error.
|
||||
* @param {string} text
|
||||
* @returns {unknown}
|
||||
*/
|
||||
export default (text) => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (_err) {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { SUPPORTED_FILE_TYPES } from "./constants.js";
|
||||
import crel from "../common/crel.js";
|
||||
import * as base64url from "../common/base64url.js";
|
||||
import { routeFile } from "./fileRouter.js";
|
||||
|
||||
const accept = SUPPORTED_FILE_TYPES
|
||||
|
@ -45,12 +46,7 @@ const disclaimerParagraphEl = crel(
|
|||
const formatBytes = async (blob) => {
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const string = String.fromCharCode(...bytes);
|
||||
const base64 = btoa(string);
|
||||
return base64
|
||||
.replaceAll("+", "-")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "");
|
||||
return base64url.stringify(bytes);
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>formats.exposed/png</title>
|
||||
<noscript>
|
||||
<meta http-equiv="refresh" content="0; url=.." />
|
||||
</noscript>
|
||||
<link rel="stylesheet" href="../common/global.css">
|
||||
<link rel="stylesheet" href="png.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<div id="error" hidden></div>
|
||||
<div id="explorer" hidden>
|
||||
<div id="bytesEl"></div>
|
||||
<div id="treeEl"></div>
|
||||
</div>
|
||||
<script src="png.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,41 @@
|
|||
// @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 };
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
// @ts-check
|
||||
|
||||
import parseHash from "./parseHash.js";
|
||||
|
||||
const errorEl = document.getElementById("error");
|
||||
const explorerEl = document.getElementById("explorer");
|
||||
if (!errorEl || !explorerEl) throw new Error("HTML is not set up correctly");
|
||||
|
||||
const main = () => {
|
||||
// TODO: We may want a better UI here.
|
||||
const parsedHash = parseHash(location.hash);
|
||||
if (!parsedHash) {
|
||||
location.href = "..";
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Actually do something!
|
||||
console.log(parsedHash);
|
||||
};
|
||||
|
||||
main();
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,47 @@
|
|||
import { assertEquals } from "assert";
|
||||
import * as base64url from "../../public/common/base64url.js";
|
||||
|
||||
/**
|
||||
* A shorthand for `new Uint8Array()`.
|
||||
*/
|
||||
const b = (...bytes: number[]) => new Uint8Array(bytes);
|
||||
|
||||
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}`);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { assertEquals } from "assert";
|
||||
import maybeJsonParse from "../../public/common/maybeJsonParse.js";
|
||||
|
||||
Deno.test("returns null if string can't be parsed", () => {
|
||||
assertEquals(maybeJsonParse(""), null);
|
||||
assertEquals(maybeJsonParse('{"hi":'), null);
|
||||
});
|
||||
|
||||
Deno.test("parses valid JSON", () => {
|
||||
assertEquals(maybeJsonParse("123"), 123);
|
||||
assertEquals(maybeJsonParse('{"hi": 5}'), { hi: 5 });
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { assertEquals } from "assert";
|
||||
import { stub } from "mock";
|
||||
import parseHash from "../../public/png/parseHash.js";
|
||||
|
||||
Deno.test("returns null if hash cannot be parsed", () => {
|
||||
const testCases = [
|
||||
// Missing fields
|
||||
"#null",
|
||||
"#{}",
|
||||
"#{%22name%22:%22image.png%22}",
|
||||
"#{%22bytes%22:%22AQID%22}",
|
||||
// Invalid JSON
|
||||
"",
|
||||
"#{%22name%22:%22small.png%22,%22bytes%22:%22iAQID",
|
||||
];
|
||||
|
||||
const warnStub = stub(console, "warn");
|
||||
|
||||
try {
|
||||
for (const testCase of testCases) {
|
||||
assertEquals(
|
||||
parseHash(testCase),
|
||||
null,
|
||||
`Parsing ${testCase} should fail`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
warnStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
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]),
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue