Catalog: previews decode in full, zones checked, prose addresses trimmed, install steps left to maintainers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-04 21:04:44 -04:00
1 parent 8775745d9d
commit ab42b6f05b
5 files changed
+72 -27

No files matched your search

+1 -1
View File
@@ -40,7 +40,7 @@ Use your desktop's window capture, such as GNOME's Print Screen and its Window m
- Write characters as they are, not as `&...;` references.
- Any address in the text is the `website` or the `source`.
`pnpm validate` checks the size, the links, the images and the markup.
`pnpm validate` checks the size, the links, the addresses, the images and the markup. Maintainers read the words: a README that tells how to install is sent back.
## Pull request
+1
View File
@@ -9,6 +9,7 @@
},
"dependencies": {
"marked": "18.0.14",
"pngjs": "7.0.0",
"twitter-text": "3.1.0"
}
}
+9
View File
@@ -11,6 +11,9 @@ importers:
marked:
specifier: 18.0.14
version: 18.0.14
pngjs:
specifier: 7.0.0
version: 7.0.0
twitter-text:
specifier: 3.1.0
version: 3.1.0
@@ -30,6 +33,10 @@ packages:
engines: {node: '>= 20'}
hasBin: true
pngjs@7.0.0:
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
engines: {node: '>=14.19.0'}
punycode@1.4.1:
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
@@ -47,6 +54,8 @@ snapshots:
marked@18.0.14: {}
pngjs@7.0.0: {}
punycode@1.4.1: {}
twemoji-parser@11.0.2: {}
+42 -21
View File
@@ -1,7 +1,8 @@
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { crc32, inflateSync } from "node:zlib";
import { inflateSync } from "node:zlib";
import { Lexer } from "marked";
import { PNG as Decoder } from "pngjs";
import twitter from "twitter-text";
export const CATEGORIES = {
@@ -20,7 +21,7 @@ const REQUIRED = ["id", "name", "author", "description", "category", "platforms"
const OPTIONAL = ["building", "stars", "starsUpdatedAt"];
const TIME = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(\.\d+)?(Z|[+-]\d\d:\d\d)$/;
const ENTITY = /&(#\d+|#x[0-9a-f]+|[a-z][a-z0-9]*);/i;
const URLISH = /[a-z][a-z0-9+.-]*:\/\/[^\s<>()\[\]"'`]*/gi;
const URLISH = /[a-z][a-z0-9+.-]*:\/\/[^\s<>\[\]"'`]*/gi;
const TOKENS = new Set(["space", "heading", "paragraph", "text", "strong", "em", "del", "br", "hr", "blockquote", "list", "list_item", "table", "escape", "checkbox", "def", "link", "image"]);
/** Twitter's weighted length. */
@@ -32,7 +33,16 @@ function time(value) {
if (!m) return false;
const [y, mo, d, h, mi, sec] = m.slice(1, 7).map(Number);
const at = new Date(Date.UTC(y, mo - 1, d, h, mi, sec));
return at.getUTCMonth() === mo - 1 && at.getUTCDate() === d && h < 24 && mi < 60 && sec < 60;
const zone = m[8] === "Z" || (Number(m[8].slice(1, 3)) < 24 && Number(m[8].slice(4)) < 60);
return zone && at.getUTCMonth() === mo - 1 && at.getUTCDate() === d && h < 24 && mi < 60 && sec < 60 && !Number.isNaN(Date.parse(value));
}
/** An address as written in prose, less the punctuation that ends a sentence. */
function trim(url) {
let out = url.replace(/[.,;:!?*_~]+$/, "");
const count = (c) => out.split(c).length - 1;
while (out.endsWith(")") && count(")") > count("(")) out = out.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
return out;
}
function fail(where, what) {
@@ -63,31 +73,42 @@ export function github(source) {
return m ? { owner: m[1], repo: m[2] } : null;
}
const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
const ADAM7 = [[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2]];
/** The bytes a PNG's image data must inflate to, from its header. */
function rasterBytes(bytes) {
const [w, h, depth, color, interlace] = [bytes.readUInt32BE(16), bytes.readUInt32BE(20), bytes[24], bytes[25], bytes[28]];
const row = (width) => 1 + Math.ceil((width * depth * CHANNELS[color]) / 8);
if (!interlace) return h * row(w);
return ADAM7.reduce((n, [x, y, dx, dy]) => {
const [pw, ph] = [Math.ceil((w - x) / dx), Math.ceil((h - y) / dy)];
return pw > 0 && ph > 0 ? n + ph * row(pw) : n;
}, 0);
}
/** The PNG's image data, inflated. */
function raster(bytes) {
const data = [];
for (let at = 8; at + 8 <= bytes.length; ) {
const length = bytes.readUInt32BE(at);
if (bytes.toString("latin1", at + 4, at + 8) === "IDAT") data.push(bytes.subarray(at + 8, at + 8 + length));
at += 12 + length;
}
return inflateSync(Buffer.concat(data));
}
function png(where, dir, name) {
if (!FILE.test(name) || !name.endsWith(".png")) fail(where, `${name} must be a lowercase .png file name in the app folder`);
const path = join(dir, name);
if (!existsSync(path)) fail(where, `${name} is missing`);
const bytes = readFileSync(path);
if (!bytes.subarray(0, 8).equals(PNG)) fail(where, `${name} is not a PNG`);
const types = [];
const data = [];
for (let at = 8; at < bytes.length; ) {
if (at + 12 > bytes.length) fail(where, `${name} is cut short`);
const length = bytes.readUInt32BE(at);
const end = at + 8 + length;
if (end + 4 > bytes.length) fail(where, `${name} is cut short`);
const type = bytes.toString("latin1", at + 4, at + 8);
if (crc32(bytes.subarray(at + 4, end)) !== bytes.readUInt32BE(end)) fail(where, `${name} has a bad ${type} checksum`);
if (type === "IHDR" && (length !== 13 || !bytes.readUInt32BE(at + 8) || !bytes.readUInt32BE(at + 12))) fail(where, `${name} has a bad header`);
if (type === "IDAT") data.push(bytes.subarray(at + 8, end));
types.push(type);
at = end + 4;
}
if (types[0] !== "IHDR" || types.at(-1) !== "IEND" || !data.length) fail(where, `${name} is not a whole PNG`);
try {
inflateSync(Buffer.concat(data));
if (raster(bytes).length !== rasterBytes(bytes)) throw new Error("image data does not fill the image");
Decoder.sync.read(bytes);
} catch (error) {
fail(where, `${name} has broken image data: ${error.message}`);
fail(where, `${name} does not decode: ${error.message}`);
}
}
@@ -135,7 +156,7 @@ function readme(dir, app) {
const markdown = bytes.toString("utf8");
const links = [app.website, app.source].filter(Boolean);
if (ENTITY.test(markdown)) fail(where, `write characters as they are, not as references: ${markdown.match(ENTITY)[0]}`);
for (const url of markdown.replaceAll("\\", "").match(URLISH) ?? []) if (!links.includes(url)) fail(where, `addresses go only to the website or source: ${url}`);
for (const url of markdown.replaceAll("\\", "").match(URLISH) ?? []) if (!links.includes(trim(url))) fail(where, `addresses go only to the website or source: ${url}`);
const walk = (tokens) => {
for (const t of tokens) {
if (!TOKENS.has(t.type)) fail(where, `${t.type === "html" ? "raw HTML" : t.type === "code" ? "a code block" : t.type} is not allowed: ${t.raw.trim().slice(0, 60)}`);
+19 -5
View File
@@ -15,9 +15,13 @@ function chunk(type, data) {
return Buffer.concat([head, data, sum]);
}
/** A one-pixel gray PNG. */
const header = Buffer.from([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0]);
const PNG = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk("IHDR", header), chunk("IDAT", deflateSync(Buffer.from([0, 128]))), chunk("IEND", Buffer.alloc(0))]);
/** 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;
@@ -55,6 +59,12 @@ test("a good app reads back with its README", () => {
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("the empty catalog reads", () => {
const root = mkdtempSync(join(tmpdir(), "showcase-"));
mkdirSync(join(root, "apps"));
@@ -76,11 +86,15 @@ const bad = [
["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) } }, /cut short/],
["a PNG with a bad checksum", { app: { ...good, previews: ["bad.png"] }, files: { "bad.png": broken } }, /checksum/],
["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&commat;evil.com/" } }, /plain host/],
["a host with credentials", { app: { ...good, website: "https://good.example@evil.com/" } }, /plain host/],