Public: anyone can read this; the crew can change it.

A font file (ttf, otf, woff, woff2) in a repository renders as a specimen #10374

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/10282-a-font-file-ttf-otf-woff-woff2-in-a-repo → main 5 commits fork 3 threads · 3 unresolved

  • Merged. scout/10282-a-font-file-ttf-otf-woff-woff2-in-a-repo was merged into main as 84d0ceb.

Review

Sign in to review this change.
11 files changed +313 −3
docs/design/areas/code.md +2
@@ -10,3 +10,5 @@
1010 - `.card.bare` — a card with its frame taken off (`card({ extra: "bare" })`): no border, shadow, fill or padding, no rules between rows, head and foot as plain lines. The repository page's file listing, README, Following panel and fork line use it; the markup, and so every hook, stays the same.
1111
1212 - `.moved-from` (#10109) — the one muted line "Moved from <old path> on <date>" under a repository head for 30 days after a move; the path wraps anywhere, so no sideways scroll at 390px.
13
14 A font specimen (`render/font.ts`): `.font-view` sits in a bare card (`card({ extra: "bare" })`, no box of its own); `.font-name` its family heading, `.font-line` one line of sample text, and `.font-s`, `.font-m`, `.font-l` its three sizes. The small size is 14px; the larger two are display sizes, off the UI scale on purpose.
docs/features/font.md added +7
@@ -0,0 +1,7 @@
1 # Font specimens
2
3 Open a `.ttf`, `.otf`, `.woff` or `.woff2` file in a repository and the blob page draws a specimen (`src/web/render/font.ts`): the family name, then the alphabet, digits and a pangram at three sizes, set in the font itself. Detection is by magic bytes, so a renamed non-font stays an ordinary file.
4
5 The family comes from the `name` table of a TrueType/OpenType file (bounds-checked, escaped); a WOFF file compresses its tables, so it is named by its file name. A small script loads the font through `FontFace` from the same-origin media route; without script the specimen shows in the page's own face. Only this page's policy gains `font-src 'self'` (never `data:`), on first and kept renders alike. `?view=source`, Raw and Download show the bytes untouched.
6
7 Terminal (SSH): there is no screen for it. A terminal draws in its own fixed face and cannot load a font, so a specimen would show nothing the file view does not; the file view lists it as a binary file with its size.
src/web/layout.ts +7 −1
@@ -150,6 +150,12 @@ export interface PageOptions {
150150 * blob page of a PDF asks; no other page's policy changes.
151151 */
152152 objectSrc?: boolean;
153 /**
154 * The page sets a font file in the font itself (`render/font.ts`): the
155 * browser may load a font from this origin (`font-src 'self'`). Only the
156 * blob page of a font asks; no other page's policy changes.
157 */
158 fontSrc?: boolean;
153159 /**
154160 * The page carries a Cloudflare Turnstile widget (`/signup`): its script,
155161 * frame and verification call may come from challenges.cloudflare.com, and
@@ -882,7 +888,7 @@ export function page(options: PageOptions, status = 200): Response {
882888 // The hashed stylesheet and scripts are 'self'; the two inline <style>s
883889 // are named by hash. connect-src 'self' lets the live event stream
884890 // (EventSource) and fragment fetches reach same-origin.
885 "content-security-policy": `default-src 'none'; style-src 'self' ${styleSources}; script-src 'self'${options.wasm ? " 'wasm-unsafe-eval'" : ""}${options.turnstile ? ` ${TURNSTILE_ORIGIN}` : ""}; connect-src 'self'${options.turnstile ? ` ${TURNSTILE_ORIGIN}` : ""}; ${frameSources(options)}worker-src 'self'; manifest-src 'self'; img-src 'self' data:; ${options.objectSrc ? "object-src 'self'; " : ""}form-action 'self'; base-uri 'none'; frame-ancestors 'none'`,
891 "content-security-policy": `default-src 'none'; style-src 'self' ${styleSources}; script-src 'self'${options.wasm ? " 'wasm-unsafe-eval'" : ""}${options.turnstile ? ` ${TURNSTILE_ORIGIN}` : ""}; connect-src 'self'${options.turnstile ? ` ${TURNSTILE_ORIGIN}` : ""}; ${frameSources(options)}worker-src 'self'; manifest-src 'self'; img-src 'self' data:; ${options.objectSrc ? "object-src 'self'; " : ""}${options.fontSrc ? "font-src 'self'; " : ""}form-action 'self'; base-uri 'none'; frame-ancestors 'none'`,
886892 "referrer-policy": "same-origin",
887893 "x-content-type-options": "nosniff",
888894 // Markup is revalidated every time; only the hashed assets it names are
src/web/pages/repo.ts +17 −2
@@ -86,6 +86,7 @@ import { flashTo, fragment, page, redirect } from "../layout.ts";
8686 import { renderMarkdown } from "../markdown.ts";
8787 import { overSize } from "../render/archive.ts";
8888 import { csvJs, isCsvPath } from "../render/csv.ts";
89 import { fontJs } from "../render/font.ts";
8990 import { hasTable } from "../render/sqlite.ts";
9091 import { fileKind, HEAD_BYTES, RENDER_CAP } from "../render/kinds.ts";
9192 import { RENDERERS, rendererFor } from "../render/renderers.ts";
@@ -1113,6 +1114,15 @@ export function drawsPdf(scope: RepoScope, path: string, blob: Blob, view: BlobV
11131114 );
11141115 }
11151116
1117 /** Whether the blob page of this file sets a font in itself, and so needs `font-src`. */
1118 export function drawsFont(scope: RepoScope, path: string, blob: Blob, view: BlobView): boolean {
1119 return (
1120 view !== "source" &&
1121 RENDERERS.font !== undefined &&
1122 blobRenderer(scope, path, blob)?.render === RENDERERS.font
1123 );
1124 }
1125
11161126 /**
11171127 * One file, framed by the same path header the tree uses. The header is
11181128 * sticky and the whole pane is one element, because a script swapping a
@@ -1499,6 +1509,8 @@ export function codePage(
14991509 before?: Html;
15001510 /** The page draws a PDF in an `<object>` (`PageOptions.objectSrc`). */
15011511 objectSrc?: boolean;
1512 /** The page sets a font in itself (`PageOptions.fontSrc`). */
1513 fontSrc?: boolean;
15021514 /** Script only some files need (the CSV table's sort and filter). */
15031515 extraJs?: string;
15041516 },
@@ -1507,6 +1519,7 @@ export function codePage(
15071519 ctx,
15081520 title: o.title,
15091521 ...(o.objectSrc ? { objectSrc: true } : {}),
1522 ...(o.fontSrc ? { fontSrc: true } : {}),
15101523 crumbs: scope.crumbs,
15111524 palette: scope.palette,
15121525 body: html`${o.header ?? scopeHeader(ctx, scope, "code", { compact: true })}
@@ -1669,7 +1682,7 @@ export async function blobPage(ctx: Ctx): Promise<Response> {
16691682 explorer: hit.explorer,
16701683 content: hit.content,
16711684 objectSrc: hit.objectSrc,
1672 ...(hit.csv ? { extraJs: csvJs } : {}),
1685 ...(hit.font ? { fontSrc: true, extraJs: fontJs } : hit.csv ? { extraJs: csvJs } : {}),
16731686 })),
16741687 );
16751688 }
@@ -1727,12 +1740,14 @@ export async function blobPage(ctx: Ctx): Promise<Response> {
17271740 ),
17281741 });
17291742 const objectSrc = drawsPdf(scope, path, shown, view);
1743 const font = drawsFont(scope, path, shown, view);
17301744 if (kept && keepable)
17311745 keepBlobPage(repo.id, ref, path, `${view}\0${table ?? ""}`, all, {
17321746 title: `${filename} · ${repo.name}`,
17331747 explorer: explorerHtml,
17341748 content: pane,
17351749 objectSrc,
1750 font,
17361751 csv: isCsvPath(path),
17371752 });
17381753 const response = await keepFinished(codePage(ctx, scope, {
@@ -1741,7 +1756,7 @@ export async function blobPage(ctx: Ctx): Promise<Response> {
17411756 explorer: explorerHtml,
17421757 content: pane,
17431758 objectSrc,
1744 ...(isCsvPath(path) ? { extraJs: csvJs } : {}),
1759 ...(font ? { fontSrc: true, extraJs: fontJs } : isCsvPath(path) ? { extraJs: csvJs } : {}),
17451760 }));
17461761 notePhase(ctx.request, "render", paneMs + performance.now() - pageStart);
17471762 return immutableWhenAddressed(ctx.request, ctx.auth.user === undefined, repo, ref, response);
src/web/pages/tree_cache.ts +1
@@ -114,6 +114,7 @@ export interface RenderedBlob extends RenderedTree {
114114 readonly title: string;
115115 readonly objectSrc: boolean;
116116 readonly csv: boolean;
117 readonly font: boolean;
outdated

font is optional while objectSrc and csv beside it are required. Make it required so a cached entry cannot silently lose font-src, and add a test that hits the cache.

117118 }
118119
119120 const blobKey = (repoId: string, commit: string, path: string, view: string, all: boolean): string =>
src/web/render/font.ts added +102
@@ -0,0 +1,102 @@
1 /**
2 * A font file drawn as a specimen: its family name, then the alphabet, the
3 * digits and one pangram at three sizes, set in the font itself.
4 *
5 * The family comes from the `name` table of a TrueType or OpenType file; a
6 * WOFF file compresses its tables, so it is named by its file name. The font
7 * is loaded by `fontJs` (a `FontFace` from the same-origin media route), so
8 * the page's policy gains `font-src 'self'` and nothing else (`layout.ts`
9 * `fontSrc`); never a `data:` URL. Without script the specimen shows in the
10 * page's own face, still named and sized.
11 */
12
13 import { html } from "../html.ts";
14 import { card, fileSize } from "../ui.ts";
15 import type { Rendered, RenderInput } from "./renderers.ts";
16
17 const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz";
18 const DIGITS = "0123456789 .,;:!?&@#%()[]{}";
19 const PANGRAM = "Sphinx of black quartz, judge my vow.";
20
21 const u16 = (v: DataView, at: number): number => v.getUint16(at, false);
22
23 /**
24 * The family name of a TrueType/OpenType file, or undefined when the bytes
25 * do not hold one. Every offset is bounds-checked: the file is untrusted.
26 */
27 export function fontFamily(body: Uint8Array): string | undefined {
28 if (body.length < 12) return undefined;
29 const v = new DataView(body.buffer, body.byteOffset, body.byteLength);
30 const tables = Math.min(u16(v, 4), 128);
31 for (let i = 0; i < tables; i++) {
32 const rec = 12 + i * 16;
33 if (rec + 16 > body.length) return undefined;
34 if (String.fromCharCode(...body.subarray(rec, rec + 4)) !== "name") continue;
35 const base = v.getUint32(rec + 8, false);
36 const length = v.getUint32(rec + 12, false);
37 if (base + 6 > body.length || length < 6) return undefined;
38 const count = Math.min(u16(v, base + 2), 2048);
39 const strings = base + u16(v, base + 4);
40 let best: { rank: number; text: string } | undefined;
41 for (let n = 0; n < count; n++) {
42 const r = base + 6 + n * 12;
43 if (r + 12 > body.length) break;
44 const platform = u16(v, r);
45 const id = u16(v, r + 6);
46 const len = u16(v, r + 8);
47 const from = strings + u16(v, r + 10);
48 if ((id !== 16 && id !== 1) || from + len > body.length || len === 0) continue;
49 let text: string;
50 if (platform === 3 || platform === 0) {
51 if (len % 2 !== 0) continue;
52 text = "";
53 for (let o = from; o < from + len; o += 2) text += String.fromCharCode(u16(v, o));
54 } else if (platform === 1) {
55 text = String.fromCharCode(...body.subarray(from, from + len));
56 } else continue;
57 text = text.replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 100);
58 if (text === "") continue;
59 // The typographic family (16) over the legacy one (1); Windows over Mac.
60 const rank = (id === 16 ? 2 : 0) + (platform === 1 ? 0 : 1);
61 if (best === undefined || rank > best.rank) best = { rank, text };
62 }
63 return best?.text;
64 }
65 return undefined;
66 }
67
68 /** The file name without directory or extension, for a font that names no family. */
69 const stem = (path: string): string => {
70 const name = path.slice(path.lastIndexOf("/") + 1);
71 const dot = name.lastIndexOf(".");
72 return dot > 0 ? name.slice(0, dot) : name;
73 };
74
75 export function font(input: RenderInput): Rendered {
76 const isSfnt = input.body.length >= 4 && !/^(wOFF|wOF2)/.test(String.fromCharCode(...input.body.subarray(0, 4)));
77 const family = (isSfnt ? fontFamily(input.body) : undefined) ?? stem(input.path);
78 const line = (size: string, text: string) => html`<p class="font-line font-${size}">${text}</p>`;
79 return {
80 lines: 0,
81 summary: `${fileSize(input.size)} · font`,
82 content: card(html`<div class="font-view" data-font="${input.media}">
outdated

Principles 6 and 7: a bordered, shadowed card around the specimen is a box around a thing. Use card({ extra: "bare" }) as the rest of the code area does, and keep the rows as plain lines.

83 <h2 class="font-name">${family}</h2>
84 ${line("s", ALPHABET)} ${line("s", DIGITS)} ${line("s", PANGRAM)}
85 ${line("m", ALPHABET)} ${line("m", DIGITS)} ${line("m", PANGRAM)}
86 ${line("l", "Aa Bb Cc 123")} ${line("l", PANGRAM)}
87 </div>`, { extra: "bare", pad: false }),
88 };
89 }
90
91 /** Loads the font from the media route and lets the specimen use it. Trusted static code. */
92 export const fontJs = /* js */ `
93 (function () {
94 var box = document.querySelector('.font-view[data-font]');
95 if (!box || !window.FontFace) return;
96 var face = new FontFace('ForgeSpecimen', 'url(' + JSON.stringify(box.getAttribute('data-font')) + ')');
97 face.load().then(function () {
98 document.fonts.add(face);
99 box.classList.add('font-ready');
100 }, function () {});
101 })();
102 `;
src/web/render/kinds.ts +6
@@ -22,6 +22,7 @@ export type FileKind =
2222 | "sqlite"
2323 | "model3d"
2424 | "archive"
25 | "font"
2526 | "config"
2627 | "markdown"
2728 | "log"
@@ -47,6 +48,7 @@ export const RENDER_CAP: Readonly<Record<FileKind, number>> = {
4748 sqlite: 5 * MB,
4849 model3d: 100 * MB,
4950 archive: 50 * MB,
51 font: 20 * MB,
5052 config: 1 * MB,
5153 markdown: 1 * MB,
5254 log: 1 * MB,
@@ -126,6 +128,10 @@ const MEDIA: Readonly<Record<string, Media>> = {
126128 sqlite: { kind: "sqlite", name: "SQLite", type: "application/vnd.sqlite3", magic: ascii("SQLite format 3\0") },
127129 sqlite3: { kind: "sqlite", name: "SQLite", type: "application/vnd.sqlite3", magic: ascii("SQLite format 3\0") },
128130 stl: { kind: "model3d", name: "STL", type: "model/stl" },
131 ttf: { kind: "font", name: "TrueType", type: "font/ttf", magic: bytes(0x00, 0x01, 0x00, 0x00) },
132 otf: { kind: "font", name: "OpenType", type: "font/otf", magic: ascii("OTTO") },
133 woff: { kind: "font", name: "WOFF", type: "font/woff", magic: ascii("wOFF") },
134 woff2: { kind: "font", name: "WOFF2", type: "font/woff2", magic: ascii("wOF2") },
129135 glb: { kind: "model3d", name: "glTF binary", type: "model/gltf-binary", magic: ascii("glTF") },
130136 };
131137
src/web/render/renderers.ts +2
@@ -11,6 +11,7 @@ import { audio } from "./audio.ts";
1111 import { config } from "./config.ts";
1212 import { isFormConfig } from "./config-format.ts";
1313 import { csv } from "./csv.ts";
14 import { font } from "./font.ts";
1415 import { image } from "./image.ts";
1516 import { claimedMedia, fileKind, HEAD_BYTES, NO_BYTES, RENDER_CAP, type FileKind } from "./kinds.ts";
1617 import { log, hasEscape } from "./log.ts";
@@ -65,6 +66,7 @@ export const RENDERERS: Partial<Readonly<Record<FileKind, Renderer>>> = {
6566
6667 subtitles,
6768 archive,
69 font,
6870 };
6971
7072 /**
src/web/style/content.ts +8
@@ -475,6 +475,14 @@ form.inline { display: inline; margin-left: auto; }
475475 /* A phone: a shorter frame, so a thumb can scroll the page past it. */
476476 @media (max-width: 600px) { .pdf-frame { height: 60vh; min-height: 360px; } }
477477
478 /* A font specimen: set in the font itself once the script has loaded it from
479 the media route; until then (or without script) in the page's own face. */
480 .font-view { padding: 16px 20px; }
481 .font-name { margin: 0 0 12px; font-size: 1.75rem; line-height: 1.3; overflow-wrap: anywhere; }
482 .font-line { margin: 0 0 10px; line-height: 1.4; overflow-wrap: anywhere; }
483 .font-s { font-size: 14px; } .font-m { font-size: 1.5rem; } .font-l { font-size: 3rem; line-height: 1.2; }
484 .font-view.font-ready .font-name, .font-view.font-ready .font-line { font-family: "ForgeSpecimen", sans-serif; }
485
478486 /* ---- image view ---- */
479487 .imgview-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 4px 16px; font-size: 12.5px; border-bottom: 1px solid var(--border); }
480488 .imgview-dim { min-width: 9ch; }
test/render-font.test.ts added +157
@@ -0,0 +1,157 @@
1 /** A font file on the blob page: kind, family name, specimen, and a CSP that only it relaxes. */

Add a second-request (kept page) assertion for font-src and the script, and docs/features/font.md with the how-to. The terminal door (src/tui/) is also missing.

2
3 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp, type App } from "../src/app.ts";
8 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { fileKind } from "../src/web/render/kinds.ts";
11 import { fontFamily } from "../src/web/render/font.ts";
12 import { seedAdmin } from "./helpers/admin.ts";
13 import { quietLogs } from "./helpers/globals.ts";
14
15 quietLogs();
16
17 /** A minimal sfnt: one `name` table holding a Windows (UTF-16BE) family name. */
18 function sfnt(magic: number[], family: string): Uint8Array {
19 const text = new Uint8Array(family.length * 2);
20 for (let i = 0; i < family.length; i++) text[i * 2 + 1] = family.charCodeAt(i);
21 const table = new Uint8Array(6 + 12 + text.length);
22 const t = new DataView(table.buffer);
23 t.setUint16(2, 1); // one record
24 t.setUint16(4, 18); // string storage offset
25 t.setUint16(6, 3); // platform: Windows
26 t.setUint16(8, 1);
27 t.setUint16(10, 0x409);
28 t.setUint16(12, 1); // nameID 1: family
29 t.setUint16(14, text.length);
30 t.setUint16(16, 0);
31 table.set(text, 18);
32 const out = new Uint8Array(12 + 16 + table.length);
33 out.set(magic, 0);
34 const v = new DataView(out.buffer);
35 v.setUint16(4, 1);
36 out.set(new TextEncoder().encode("name"), 12);
37 v.setUint32(20, 28);
38 v.setUint32(24, table.length);
39 out.set(table, 28);
40 return out;
41 }
42
43 const TTF = sfnt([0, 1, 0, 0], "Forge <Sans>");
44 const OTF = sfnt([0x4f, 0x54, 0x54, 0x4f], "Forge Serif");
45 const WOFF = new Uint8Array([0x77, 0x4f, 0x46, 0x46, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
46 const FAKE = new TextEncoder().encode("this is plainly not a font\n");
47
48 describe("font kind and family", () => {
49 test("detected by magic bytes, and a renamed non-font is not a font", () => {
50 expect(fileKind("a.ttf", TTF)).toBe("font");
51 expect(fileKind("a.otf", OTF)).toBe("font");
52 expect(fileKind("a.woff", WOFF)).toBe("font");
53 expect(fileKind("a.woff2", new TextEncoder().encode("wOF2\0\0"))).toBe("font");
54 expect(fileKind("fake.ttf", FAKE)).toBe("text");
55 expect(fileKind("fake.ttf", new Uint8Array([1, 2, 0, 3]))).toBe("binary");
56 expect(fileKind("a.otf", TTF)).not.toBe("font");
57 });
58
59 test("the family is read from the name table", () => {
60 expect(fontFamily(TTF)).toBe("Forge <Sans>");
61 expect(fontFamily(OTF)).toBe("Forge Serif");
62 });
63
64 test("a truncated or hostile table is no family, not a crash", () => {
65 expect(fontFamily(TTF.subarray(0, 30))).toBeUndefined();
66 const bad = TTF.slice();
67 new DataView(bad.buffer).setUint32(20, 0xfffffff0);
68 expect(fontFamily(bad)).toBeUndefined();
69 expect(fontFamily(new Uint8Array(3))).toBeUndefined();
70 });
71 });
72
73 let dataDir = "";
74 let app: App;
75 let servers: Servers;
76
77 beforeAll(async () => {
78 dataDir = await mkdtemp(join(tmpdir(), "forge-font-"));
79 app = createApp({ dataDir, port: 0 });
80 servers = await serve(app);
81 const a = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "a-long-font-passphrase" });
82 if (!a.ok) throw new Error("signup failed");
83 const repo = await createRepo(app, a.value, { name: "type", visibility: "public" });
84 if (!repo.ok) throw new Error("repo failed");
85 const made = await commitToBranch(app, a.value, repo.value, {
86 message: "fonts",
87 files: [
88 { path: "a.ttf", content: TTF },
89 { path: "b.woff", content: WOFF },
90 { path: "fake.ttf", content: FAKE },
91 { path: "n.txt", content: new TextEncoder().encode("hi\n") },
92 ],
93 });
94 if (!made.ok) throw new Error("commit failed");
95 });
96
97 afterAll(async () => {
98 await servers.stop();
99 app.close();
100 await rm(dataDir, { recursive: true, force: true });
101 });
102
103 const get = (path: string) => fetch(`${servers.url}/alice/type/${path}`);
104
105 describe("font blob pages", () => {
106 test("a ttf is a specimen named by its family, escaped, with script and font-src", async () => {
107 const res = await get("blob/main/a.ttf");
108 const html = (await res.text()).replace(/\s+/g, " ");
109 expect(html).toContain('<h2 class="font-name">Forge &lt;Sans&gt;</h2>');
110 expect(html).toContain('data-font="/alice/type/media/main/a.ttf"');
111 expect(html).toContain("Sphinx of black quartz");
112 for (const size of ["font-s", "font-m", "font-l"]) expect(html).toContain(size);
113 const csp = res.headers.get("content-security-policy")!;
114 expect(csp).toContain("font-src 'self';");
115 expect(csp).not.toMatch(/font-src[^;]*data:/);
116 expect(res.headers.get("x-content-type-options")).toBe("nosniff");
117 });
118
119 test("the kept page, served again, still has font-src and the script", async () => {
120 const first = await get("blob/main/a.ttf");
121 await first.text();
122 const res = await get("blob/main/a.ttf");
123 const html = await res.text();
124 expect(res.headers.get("content-security-policy")).toContain("font-src 'self';");
125 expect(html).toContain('data-font="/alice/type/media/main/a.ttf"');
126 // The script is a hashed asset; the kept page must name the one that draws the specimen.
127 const src = /<script src="(\/assets\/page-[^"]+\.js)"/.exec(html)?.[1];
128 expect(src).toBeDefined();
129 expect(await (await fetch(`${servers.url}${src}`)).text()).toContain("FontFace");
130 });
131
132 test("a woff is named by its file name", async () => {
133 const html = await (await get("blob/main/b.woff")).text();
134 expect(html).toContain('<h2 class="font-name">b</h2>');
135 });
136
137 test("the bytes are served as a font type", async () => {
138 const res = await get("media/main/a.ttf");
139 expect(res.headers.get("content-type")).toBe("font/ttf");
140 expect(res.headers.get("x-content-type-options")).toBe("nosniff");
141 });
142
143 test("a renamed non-font is plain source and its media is octet-stream", async () => {
144 const res = await get("blob/main/fake.ttf");
145 const html = await res.text();
146 expect(html).not.toContain("font-view");
147 expect(res.headers.get("content-security-policy")).not.toContain("font-src");
148 expect((await get("media/main/fake.ttf")).headers.get("content-type")).toBe("application/octet-stream");
149 });
150
151 test("no other page gains font-src, the source view included", async () => {
152 for (const path of ["blob/main/n.txt", "blob/main/a.ttf?view=source", ""]) {
153 const csp = (await get(path)).headers.get("content-security-policy")!;
154 expect(csp, path).not.toContain("font-src");
155 }
156 });
157 });
test/render-kinds.test.ts +4
@@ -52,6 +52,10 @@ const SAMPLES: Record<string, [Uint8Array, FileKind, string]> = {
5252 toml: [enc("a = 1\n"), "config", "application/octet-stream"],
5353 ini: [enc("[a]\n"), "config", "application/octet-stream"],
5454 conf: [enc("a 1\n"), "config", "application/octet-stream"],
55 ttf: [of(0, 1, 0, 0, 0, 0), "font", "font/ttf"],
56 otf: [enc("OTTO\0\0"), "font", "font/otf"],
57 woff: [enc("wOFF\0\0"), "font", "font/woff"],
58 woff2: [enc("wOF2\0\0"), "font", "font/woff2"],
5559 zip: [of(0x50, 0x4b, 3, 4, 0), "archive", "application/octet-stream"],
5660 tar: [enc("x".repeat(257) + "ustar\0"), "archive", "application/octet-stream"],
5761 tgz: [of(0x1f, 0x8b, 8), "archive", "application/octet-stream"],

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs