Public: anyone can read this; the crew can change it.
A test pins what a person's attached look accepts: `readImage` and `cleanNote` at their edges #6770
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6749-a-test-pins-what-a-person-s-attached-loo → main 1 commit fork
1 file
changed +110
−0
test/attach-look-edges.test.ts added +110
| @@ -0,0 +1,110 @@ | ||
| 1 | /** | |
| 2 | * What a person's attached look accepts, at its edges (#1066): `readImage` | |
| 3 | * reads base64 as bytes and refuses what is not a PNG or JPEG of a size we | |
| 4 | * hold; `cleanNote` keeps one short line, counted in code points. | |
| 5 | */ | |
| 6 | ||
| 7 | import { describe, expect, test } from "bun:test"; | |
| 8 | import { | |
| 9 | ATTACH_MAX_BYTES, | |
| 10 | ATTACH_NOTE_MAX, | |
| 11 | cleanNote, | |
| 12 | readImage, | |
| 13 | } from "../src/actions/attach_look.ts"; | |
| 14 | import { encodePng } from "../src/design/image/png.ts"; | |
| 15 | ||
| 16 | const b64 = (bytes: Uint8Array) => Buffer.from(bytes).toString("base64"); | |
| 17 | ||
| 18 | /** Just enough PNG to read its header: signature and a 13-byte IHDR. */ | |
| 19 | function pngHead(width: number, height: number): Uint8Array { | |
| 20 | const out = new Uint8Array(33); | |
| 21 | out.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); | |
| 22 | const v = new DataView(out.buffer); | |
| 23 | v.setUint32(8, 13); | |
| 24 | out.set([0x49, 0x48, 0x44, 0x52], 12); | |
| 25 | v.setUint32(16, width); | |
| 26 | v.setUint32(20, height); | |
| 27 | out[24] = 8; | |
| 28 | out[25] = 6; | |
| 29 | return out; | |
| 30 | } | |
| 31 | ||
| 32 | /** SOI, then a baseline start-of-frame of one component. */ | |
| 33 | function jpegHead(width: number, height: number): Uint8Array { | |
| 34 | return Uint8Array.from([ | |
| 35 | 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 8, | |
| 36 | height >> 8, height & 0xff, width >> 8, width & 0xff, | |
| 37 | 1, 1, 0x11, 0, | |
| 38 | ]); | |
| 39 | } | |
| 40 | ||
| 41 | describe("readImage", () => { | |
| 42 | test("base64 that is not a multiple of 4, or has a stray character, is bad_image", () => { | |
| 43 | const good = b64(encodePng(1, 1, new Uint8Array(4))); | |
| 44 | expect(readImage(good).ok).toBe(true); | |
| 45 | for (const bad of [good.replace(/=+$/, "").slice(0, -1) + "A", "abc", "QUJD*A==", "QU JD", "", "=AAA", "AAAA\n"]) { | |
| 46 | const r = readImage(bad); | |
| 47 | expect(r).toEqual({ ok: false, error: "bad_image" } as never); | |
| 48 | } | |
| 49 | }); | |
| 50 | ||
| 51 | test("a string past the length bound is too_large before any decode", () => { | |
| 52 | // Not even base64: only the length check can have refused it. | |
| 53 | const long = "*".repeat(Math.ceil((ATTACH_MAX_BYTES * 4) / 3) + 5); | |
| 54 | const r = readImage(long); | |
| 55 | expect(r.ok).toBe(false); | |
| 56 | if (!r.ok) expect(r.error).toBe("too_large"); | |
| 57 | }); | |
| 58 | ||
| 59 | test("a minimal PNG and a minimal JPEG come back with ext and size", () => { | |
| 60 | const png = readImage(b64(encodePng(3, 2, new Uint8Array(24)))); | |
| 61 | expect(png.ok).toBe(true); | |
| 62 | if (png.ok) expect([png.value.ext, png.value.width, png.value.height]).toEqual(["png", 3, 2]); | |
| 63 | const jpg = readImage(b64(jpegHead(5, 7))); | |
| 64 | expect(jpg.ok).toBe(true); | |
| 65 | if (jpg.ok) expect([jpg.value.ext, jpg.value.width, jpg.value.height]).toEqual(["jpg", 5, 7]); | |
| 66 | }); | |
| 67 | ||
| 68 | test("a GIF header is bad_image", () => { | |
| 69 | const gif = Uint8Array.from([...Buffer.from("GIF89a"), 1, 0, 1, 0, 0, 0, 0, 0x3b]); | |
| 70 | const r = readImage(b64(gif)); | |
| 71 | expect(r.ok).toBe(false); | |
| 72 | if (!r.ok) expect(r.error).toBe("bad_image"); | |
| 73 | }); | |
| 74 | ||
| 75 | test("a PNG claiming a side over the limit is bad_image; the limit itself is kept", () => { | |
| 76 | for (const [w, h] of [[8193, 1], [1, 8193]] as const) { | |
| 77 | const r = readImage(b64(pngHead(w, h))); | |
| 78 | expect(r.ok).toBe(false); | |
| 79 | if (!r.ok) expect(r.error).toBe("bad_image"); | |
| 80 | } | |
| 81 | expect(readImage(b64(pngHead(8192, 8192))).ok).toBe(true); | |
| 82 | }); | |
| 83 | }); | |
| 84 | ||
| 85 | describe("cleanNote", () => { | |
| 86 | const bad = (raw: string) => { | |
| 87 | const r = cleanNote(raw); | |
| 88 | expect(r.ok).toBe(false); | |
| 89 | if (!r.ok) expect(r.error).toBe("bad_note"); | |
| 90 | }; | |
| 91 | ||
| 92 | test("trims", () => { | |
| 93 | const r = cleanNote(" the phone view \t"); | |
| 94 | expect(r.ok && r.value).toBe("the phone view"); | |
| 95 | }); | |
| 96 | ||
| 97 | test("a blank note, a newline and a DEL are bad_note", () => { | |
| 98 | bad(""); | |
| 99 | bad(" \t "); | |
| 100 | bad("two\nlines"); | |
| 101 | bad("del\u007fchar"); | |
| 102 | }); | |
| 103 | ||
| 104 | test("the bound counts code points, not UTF-16 units", () => { | |
| 105 | const ok = cleanNote("😀".repeat(ATTACH_NOTE_MAX)); | |
| 106 | expect(ok.ok).toBe(true); | |
| 107 | bad("😀".repeat(ATTACH_NOTE_MAX + 1)); | |
| 108 | bad("a".repeat(ATTACH_NOTE_MAX + 1)); | |
| 109 | }); | |
| 110 | }); |
new above
offline