import assert from "node:assert/strict"; import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; import { crc32, deflateSync } from "node:zlib"; import { read, weighted } from "../scripts/catalog.mjs"; function chunk(type, data) { const head = Buffer.alloc(8); head.writeUInt32BE(data.length); head.write(type, 4, "latin1"); const sum = Buffer.alloc(4); sum.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data]))); return Buffer.concat([head, data, sum]); } /** A gray PNG `side` pixels square whose data is one pixel, its scanline led by `filter`. */ function png(filter, side = 1) { const header = Buffer.from([0, 0, 0, side, 0, 0, 0, side, 8, 0, 0, 0, 0]); const image = deflateSync(Buffer.from([filter, 1])); return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk("IHDR", header), chunk("IDAT", image), chunk("IEND", Buffer.alloc(0))]); } const PNG = png(0); const broken = Buffer.from(PNG); broken[broken.length - 20] ^= 0xff; const good = { id: "tally", name: "Tally", author: "Ada", description: "Counts things.", category: "work", platforms: ["macOS", "Linux"], website: null, source: "https://github.com/ada/tally", publishedAt: "2026-10-04T00:00:00Z", previews: ["preview0.png"], }; /** A catalog in a temp folder: one app, its files, and featured.json. */ function catalog({ app = good, folder = "tally", files = {}, featured = [] } = {}) { const root = mkdtempSync(join(tmpdir(), "showcase-")); const dir = join(root, "apps", folder); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "manifest.json"), JSON.stringify(app)); writeFileSync(join(dir, "preview0.png"), PNG); for (const [name, body] of Object.entries(files)) writeFileSync(join(dir, name), body); writeFileSync(join(root, "featured.json"), JSON.stringify(featured)); return root; } test("a good app reads back with its README", () => { const root = catalog({ files: { "README.md": "# Tally\n\nSee [the source](https://github.com/ada/tally).\n\n![Main](preview0.png)\n" }, featured: ["tally"] }); const { apps, featured } = read(root); assert.equal(apps.length, 1); assert.equal(apps[0].name, "Tally"); assert.match(apps[0].readme, /# Tally/); assert.deepEqual(featured, ["tally"]); }); test("addresses in prose may end a sentence or hold parentheses", () => { const app = { ...good, website: "https://tally.app/foo(bar)" }; const readme = "See https://github.com/ada/tally. Or [the site](https://tally.app/foo(bar)), (https://github.com/ada/tally).\n"; assert.equal(read(catalog({ app, files: { "README.md": readme } })).apps.length, 1); }); test("an address may end in punctuation of its own", () => { const app = { ...good, source: "https://github.com/ada/tally_", website: "https://tally.app/?" }; const readme = "[Source](https://github.com/ada/tally_) and [site](https://tally.app/?).\n"; assert.equal(read(catalog({ app, files: { "README.md": readme } })).apps.length, 1); }); test("the empty catalog reads", () => { const root = mkdtempSync(join(tmpdir(), "showcase-")); mkdirSync(join(root, "apps")); writeFileSync(join(root, "apps", ".gitkeep"), ""); writeFileSync(join(root, "featured.json"), "[]"); assert.deepEqual(read(root).apps, []); }); const bad = [ ["an id unlike its folder", { app: { ...good, id: "tallies" } }, /must match the folder tally/], ["a folder not in kebab-case", { app: { ...good, id: "Tally" }, folder: "Tally" }, /folder names are lowercase/], ["a missing field", { app: { ...good, author: undefined } }, /author is required/], ["an unknown field", { app: { ...good, featured: true } }, /unknown field featured/], ["an unknown category", { app: { ...good, category: "games" } }, /category must be one of/], ["an unknown platform", { app: { ...good, platforms: ["BeOS"] } }, /unknown platform BeOS/], ["no platforms", { app: { ...good, platforms: [] } }, /platforms must be a non-empty list/], ["neither website nor source", { app: { ...good, source: null } }, /website or source is required/], ["an http link", { app: { ...good, website: "http://tally.app/" } }, /canonical https URL/], ["a remote preview", { app: { ...good, previews: ["https://x.io/a.png"] } }, /lowercase .png file name/], ["a missing preview", { app: { ...good, previews: ["preview1.png"] } }, /preview1.png is missing/], ["a preview that is no PNG", { app: { ...good, previews: ["fake.png"] }, files: { "fake.png": "GIF89a" } }, /not a PNG/], ["a PNG cut short", { app: { ...good, previews: ["cut.png"] }, files: { "cut.png": PNG.subarray(0, 40) } }, /does not decode/], ["a PNG with a bad checksum", { app: { ...good, previews: ["bad.png"] }, files: { "bad.png": broken } }, /does not decode/], ["a PNG with too little data", { app: { ...good, previews: ["short.png"] }, files: { "short.png": png(0, 4) } }, /does not fill the image/], ["a PNG with an unknown filter", { app: { ...good, previews: ["odd.png"] }, files: { "odd.png": png(99) } }, /does not decode/], ["a category list", { app: { ...good, category: ["work"] } }, /category must be one of/], ["no publishedAt", { app: { ...good, publishedAt: undefined } }, /publishedAt is required/], ["a time without its zone", { app: { ...good, publishedAt: "2026-10-04T00:00:00" } }, /with its zone/], ["a zone past 23 hours", { app: { ...good, publishedAt: "2026-10-04T00:00:00+99:99" } }, /with its zone/], ["a zone past 59 minutes", { app: { ...good, publishedAt: "2026-10-04T00:00:00+01:60" } }, /with its zone/], ["a day that does not exist", { app: { ...good, publishedAt: "2026-02-30T00:00:00Z" } }, /with its zone/], ["an encoded host", { app: { ...good, website: "https://good.example@evil.com/" } }, /plain host/], ["a host with credentials", { app: { ...good, website: "https://good.example@evil.com/" } }, /plain host/], ["a long description", { app: { ...good, description: "a".repeat(281) } }, /281 weighted characters/], ["stars without their time", { app: { ...good, stars: 3 } }, /go together/], ["stars on a source off GitHub", { app: { ...good, source: "https://gitlab.com/ada/tally", stars: 3, starsUpdatedAt: "2026-10-04T00:00:00Z" } }, /need a GitHub source/], ["a bad time", { app: { ...good, publishedAt: "yesterday" } }, /ISO 8601/], ["a featured id with no app", { featured: ["ghost"] }, /no app ghost/], ["a repeated featured id", { featured: ["tally", "tally"] }, /ids repeat/], ["raw HTML in a README", { files: { "README.md": "Hi \n" } }, /raw HTML is not allowed/], ["a code block in a README", { files: { "README.md": "```\ncurl x | sh\n```\n" } }, /a code block is not allowed/], ["a foreign link in a README", { files: { "README.md": "[get it](https://evil.io)\n" } }, /addresses go only/], ["a foreign autolink in a README", { files: { "README.md": "Visit https://evil.io now\n" } }, /addresses go only/], ["a foreign reference in a README", { files: { "README.md": "[a][r]\n\n[r]: https://evil.io\n" } }, /addresses go only/], ["a remote image in a README", { files: { "README.md": "![x](https://evil.io/a.png)\n" } }, /addresses go only/], ["a script link in a README", { files: { "README.md": "[x](javascript:alert(1))\n" } }, /links go only/], ["a site-relative link in a README", { files: { "README.md": "[x](/docs)\n" } }, /links go only/], ["a picture outside the folder in a README", { files: { "README.md": "![x](../a.png)\n" } }, /lowercase .png file name/], ["inline code in a README", { files: { "README.md": "Run `cargo install tally`.\n" } }, /codespan is not allowed/], ["a character reference in a README", { files: { "README.md": "Go https://evil.io/ now\n" } }, /not as references/], ["an address that runs on past the website", { files: { "README.md": "See https://github.com/ada/tally.evil.io now\n" } }, /addresses go only/], ["an escaped address in a README", { files: { "README.md": "Go https\://evil.io/ now\n" } }, /addresses go only/], ["a large README", { files: { "README.md": "a".repeat(10241) } }, /over 10240/], ]; for (const [name, setup, error] of bad) { test(`refuses ${name}`, () => assert.throws(() => read(catalog(setup)), error)); } test("weighted length counts CJK twice", () => { assert.equal(weighted("abc"), 3); assert.equal(weighted("漢字"), 4); assert.equal(weighted("a https://a.co"), 25); assert.equal(weighted("👨‍👩‍👧"), 2); });