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"
|
"check-all": "deno fmt --check && deno lint && deno task typecheck && deno test --allow-read"
|
||||||
},
|
},
|
||||||
"imports": {
|
"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": {
|
"compilerOptions": {
|
||||||
"lib": ["deno.ns", "dom"]
|
"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/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a",
|
||||||
"https://deno.land/std@0.196.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536",
|
"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/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 { SUPPORTED_FILE_TYPES } from "./constants.js";
|
||||||
import crel from "../common/crel.js";
|
import crel from "../common/crel.js";
|
||||||
|
import * as base64url from "../common/base64url.js";
|
||||||
import { routeFile } from "./fileRouter.js";
|
import { routeFile } from "./fileRouter.js";
|
||||||
|
|
||||||
const accept = SUPPORTED_FILE_TYPES
|
const accept = SUPPORTED_FILE_TYPES
|
||||||
|
@ -45,12 +46,7 @@ const disclaimerParagraphEl = crel(
|
||||||
const formatBytes = async (blob) => {
|
const formatBytes = async (blob) => {
|
||||||
const arrayBuffer = await blob.arrayBuffer();
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
const bytes = new Uint8Array(arrayBuffer);
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
const string = String.fromCharCode(...bytes);
|
return base64url.stringify(bytes);
|
||||||
const base64 = btoa(string);
|
|
||||||
return base64
|
|
||||||
.replaceAll("+", "-")
|
|
||||||
.replaceAll("/", "_")
|
|
||||||
.replaceAll("=", "");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = () => {
|
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