Ship file viewer, editor improvements, and private package releases
This commit is contained in:
1 parent
645914955c
commit
a4a31032b9
158 files changed
+10523
-137
No files matched your search
@@ -0,0 +1,200 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { extensions, kindOfExtension } from "../src/kinds.ts";
|
||||
import { RendererRegistry } from "../src/registry.ts";
|
||||
import { resolve } from "../src/resolve.ts";
|
||||
import { readBounded } from "../src/source.ts";
|
||||
import { sniff } from "../src/sniff.ts";
|
||||
|
||||
const file = (name: string, bytes: string | number[], mime = "") => ({
|
||||
name,
|
||||
blob: new Blob([typeof bytes === "string" ? bytes : new Uint8Array(bytes)], { type: mime }),
|
||||
});
|
||||
|
||||
describe("classification and signatures", () => {
|
||||
for (const [kind, exts] of Object.entries(extensions))
|
||||
for (const ext of exts) {
|
||||
test(`classifies .${ext}`, () =>
|
||||
expect(String(kindOfExtension(`.${ext.toUpperCase()}`))).toBe(kind));
|
||||
}
|
||||
const signatures: [string, string | number[], string][] = [
|
||||
["png", [137, 80, 78, 71, 13, 10, 26, 10], "ImageViewer"],
|
||||
["jpeg", [255, 216, 255], "ImageViewer"],
|
||||
["gif", "GIF89a", "ImageViewer"],
|
||||
["webp", "RIFF0000WEBP", "ImageViewer"],
|
||||
["bmp", "BM00", "ImageViewer"],
|
||||
["ico", [0, 0, 1, 0], "ImageViewer"],
|
||||
["avif", "0000ftypavif", "ImageViewer"],
|
||||
["pdf", "%PDF-1.7", "PdfViewer"],
|
||||
["wav", "RIFF0000WAVE", "MediaViewer"],
|
||||
["mp3", "ID3", "MediaViewer"],
|
||||
["aac", [255, 241, 80, 128], "MediaViewer"],
|
||||
["flac", "fLaC", "MediaViewer"],
|
||||
["ogg", "OggS", "MediaViewer"],
|
||||
["mp4", "0000ftypisom", "MediaViewer"],
|
||||
["mov", "0000ftypqt ", "MediaViewer"],
|
||||
["webm", [0x1a, 0x45, 0xdf, 0xa3], "MediaViewer"],
|
||||
["txt", "hello", "CodeViewer"],
|
||||
["js", "const value = 1;", "CodeViewer"],
|
||||
["html", "<html><script>alert(1)</script></html>", "HtmlViewer"],
|
||||
];
|
||||
for (const [ext, bytes, component] of signatures)
|
||||
test(`resolves ${ext}`, async () => {
|
||||
expect((await resolve(file(`test.${ext}`, bytes))).renderer.component).toBe(component);
|
||||
});
|
||||
test("HTML masquerading as PNG is shown as inert source", async () => {
|
||||
const plan = await resolve(
|
||||
file("fake.png", "<!doctype html><script>alert(1)</script>", "image/png"),
|
||||
);
|
||||
expect(plan.kind).toBe("markup");
|
||||
expect(plan.renderer.component).toBe("CodeViewer");
|
||||
});
|
||||
test("SVG masquerading as image never reaches native image rendering", async () => {
|
||||
const plan = await resolve(file("fake.png", '<svg onload="alert(1)"></svg>', "image/png"));
|
||||
expect(plan.kind).toBe("vector");
|
||||
expect(plan.sandboxed).toBe(true);
|
||||
expect(plan.renderer.component).toBe("SvgViewer");
|
||||
});
|
||||
test("binary or executables cannot become text through MIME", async () => {
|
||||
expect((await resolve(file("fake.txt", [0, 1, 2], "text/plain"))).renderer.component).toBe(
|
||||
"DownloadCard",
|
||||
);
|
||||
expect((await resolve(file("fake.png", "MZ123", "image/png"))).renderer.component).toBe(
|
||||
"DownloadCard",
|
||||
);
|
||||
expect((await resolve(file("fake.exe", "hello"))).renderer.component).toBe("DownloadCard");
|
||||
});
|
||||
test("UTF-8 truncated at sniff boundary is still text", () => {
|
||||
expect(sniff(new Uint8Array([97, 0xe2, 0x82])).kind).toBe("text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("degradation and network limits", () => {
|
||||
test("missing optional dependencies fall through to native renderer", async () => {
|
||||
const registry = new RendererRegistry();
|
||||
registry.register({
|
||||
kind: "pdf",
|
||||
exts: [".pdf"],
|
||||
tier: 2,
|
||||
component: "RichPdf",
|
||||
load: async () => {
|
||||
throw new Error("missing");
|
||||
},
|
||||
});
|
||||
expect((await resolve(file("test.pdf", "%PDF-1.7"), { registry })).renderer.component).toBe(
|
||||
"PdfViewer",
|
||||
);
|
||||
});
|
||||
test("overrides can be removed and conflicting signatures bypass them", async () => {
|
||||
const registry = new RendererRegistry();
|
||||
const unregister = registry.register({
|
||||
kind: "text",
|
||||
exts: ["txt"],
|
||||
tier: 2,
|
||||
component: "Custom",
|
||||
});
|
||||
expect((await resolve(file("test.txt", "hi"), { registry })).renderer.component).toBe("Custom");
|
||||
expect((await resolve(file("test.txt", "%PDF-1.7"), { registry })).renderer.component).toBe(
|
||||
"PdfViewer",
|
||||
);
|
||||
unregister();
|
||||
expect((await resolve(file("test.txt", "hi"), { registry })).renderer.component).toBe(
|
||||
"CodeViewer",
|
||||
);
|
||||
});
|
||||
test("unavailable converters degrade", async () => {
|
||||
const registry = new RendererRegistry();
|
||||
registry.register({ kind: "binary", exts: [".psd"], tier: 3, component: "Convert" });
|
||||
expect((await resolve(file("image.psd", [0, 0, 0]), { registry })).renderer.component).toBe(
|
||||
"DownloadCard",
|
||||
);
|
||||
});
|
||||
test("oversized HEAD prevents body fetch", async () => {
|
||||
const methods: string[] = [];
|
||||
const request = (async (_url, init) => {
|
||||
methods.push(init?.method ?? "GET");
|
||||
return new Response(null, { headers: { "content-length": "400000000" } });
|
||||
}) as typeof fetch;
|
||||
const plan = await resolve(
|
||||
{ name: "large.mp4", src: "https://example.test/file" },
|
||||
{ fetch: request },
|
||||
);
|
||||
expect(plan.renderer.component).toBe("DownloadCard");
|
||||
expect(methods).toEqual(["HEAD"]);
|
||||
});
|
||||
test("unknown-length or ignored Range stream is cancelled at prefix cap", async () => {
|
||||
let cancelled = false;
|
||||
const bytes = await readBounded(
|
||||
new Response(
|
||||
new ReadableStream({
|
||||
start(c) {
|
||||
c.enqueue(new Uint8Array(8192));
|
||||
},
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
}),
|
||||
),
|
||||
4096,
|
||||
true,
|
||||
);
|
||||
expect(bytes.length).toBe(4096);
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
test("full reads enforce actual bytes despite dishonest metadata", async () => {
|
||||
await expect(
|
||||
readBounded(new Response("123456", { headers: { "content-length": "1" } }), 3),
|
||||
).rejects.toThrow("limit");
|
||||
});
|
||||
test("HEAD without support still performs bounded sniff", async () => {
|
||||
const request = (async (_url, init) =>
|
||||
init?.method === "HEAD"
|
||||
? new Response(null, { status: 405 })
|
||||
: new Response("hello")) as typeof fetch;
|
||||
expect(
|
||||
(await resolve({ name: "text.txt", src: "/file" }, { fetch: request })).renderer.component,
|
||||
).toBe("CodeViewer");
|
||||
});
|
||||
test("invalid URLs, rejected fetches and key-only sources degrade", async () => {
|
||||
expect((await resolve({ name: "a.txt", src: "javascript:alert(1)" })).renderer.component).toBe(
|
||||
"DownloadCard",
|
||||
);
|
||||
expect((await resolve({ name: "a.txt", key: "private" })).renderer.component).toBe(
|
||||
"DownloadCard",
|
||||
);
|
||||
const request = (async () => {
|
||||
throw new Error("offline");
|
||||
}) as unknown as typeof fetch;
|
||||
expect(
|
||||
(await resolve({ name: "a.txt", src: "/file" }, { fetch: request })).renderer.component,
|
||||
).toBe("DownloadCard");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fallback reasons and unrenderable signatures", () => {
|
||||
test("a kind with no renderer reports why, not a size limit", async () => {
|
||||
const request = (async () =>
|
||||
new Response("PK", {
|
||||
headers: { "content-length": "512", "content-type": "application/zip" },
|
||||
})) as unknown as typeof fetch;
|
||||
const plan = await resolve(
|
||||
{ name: "bundle.zip", src: "https://example.test/bundle.zip" },
|
||||
{ fetch: request },
|
||||
);
|
||||
expect(plan.kind).toBe("archive");
|
||||
expect(plan.reason).toBe("Preview unavailable for this file");
|
||||
});
|
||||
test("design formats with ASCII-clean headers are not mistaken for text", async () => {
|
||||
const ascii = (signature: number[]) => {
|
||||
const bytes = new Uint8Array(4096).fill(0x41);
|
||||
bytes.set(signature);
|
||||
return bytes;
|
||||
};
|
||||
expect(sniff(ascii([0x38, 0x42, 0x50, 0x53])).kind).toBe("binary");
|
||||
expect(sniff(ascii([0x49, 0x49, 0x2a, 0x00])).kind).toBe("binary");
|
||||
expect(sniff(ascii([0x4d, 0x4d, 0x00, 0x2a])).kind).toBe("binary");
|
||||
expect(
|
||||
(await resolve({ name: "art.psd", blob: new Blob([ascii([0x38, 0x42, 0x50, 0x53])]) }))
|
||||
.renderer.component,
|
||||
).toBe("DownloadCard");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user