import "../setup.ts"; import { PNG } from "../helpers.ts"; import { assertEquals, assertThrows } from "@std/assert"; import { detectImageMime, scalableImage, validateImageUpload, } from "../../lib/upload.ts"; import { APIException } from "../../lib/api-exception.ts"; Deno.test("detectImageMime: sniffs magic bytes, not names", () => { assertEquals(detectImageMime(PNG), "image/png"); assertEquals( detectImageMime(new Uint8Array([0xff, 0xd8, 0xff, 0xe0])), "image/jpeg", ); assertEquals( detectImageMime(new TextEncoder().encode("GIF89a")), "image/gif", ); assertEquals( detectImageMime(new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8 ")), "image/webp", ); assertEquals( detectImageMime(new TextEncoder().encode("RIFF\0\0\0\0WAVEfmt ")), null, ); assertEquals( detectImageMime(new TextEncoder().encode("")), null, ); assertEquals(detectImageMime(new Uint8Array()), null); }); Deno.test("validateImageUpload: rejects non-images and anything over 5 MB", () => { assertEquals(validateImageUpload(PNG), "image/png"); assertThrows( () => validateImageUpload(new TextEncoder().encode("")), APIException, "not a recognised image", ); const big = new Uint8Array(5 * 1024 * 1024 + 1); big.set(PNG); assertThrows(() => validateImageUpload(big), APIException, "too large"); }); /** A JPEG's first bytes: SOI, then an EXIF segment stating `orientation`. */ function jpegWithOrientation(orientation: number, little: boolean): Uint8Array { const u16 = (n: number) => little ? [n & 0xff, n >> 8] : [n >> 8, n & 0xff]; const u32 = (n: number) => little ? [...u16(n), 0, 0] : [0, 0, ...u16(n)]; const tiff = [ ...(little ? [0x49, 0x49] : [0x4d, 0x4d]), ...u16(42), ...u32(8), ...u16(1), ...u16(0x0112), ...u16(3), ...u32(1), ...u16(orientation), ...[0, 0], ...u32(0), ]; const exif = [0x45, 0x78, 0x69, 0x66, 0, 0, ...tiff]; const length = exif.length + 2; return new Uint8Array([ ...[0xff, 0xd8], // A JFIF segment first, as a camera's file may have. ...[0xff, 0xe0, 0x00, 0x04, 0x00, 0x00], ...[0xff, 0xe1, length >> 8, length & 0xff], ...exif, ...[0xff, 0xda], ]); } Deno.test("scalableImage: a JPEG's EXIF orientation, upright without one", () => { const turn = (data: Uint8Array) => scalableImage(data)?.orientation; assertEquals(turn(jpegWithOrientation(6, false)), 6); assertEquals(turn(jpegWithOrientation(8, true)), 8); assertEquals(turn(jpegWithOrientation(42, true)), 1); assertEquals(turn(new Uint8Array([0xff, 0xd8, 0xff, 0xda, 0, 2])), 1); // Cut short of what it announces: upright, not an error. assertEquals(turn(jpegWithOrientation(6, false).subarray(0, 20)), 1); assertEquals(turn(PNG), 1); }); Deno.test("scalableImage: an animated WebP and what isn't an image are served as they are", () => { const webp = (flags: number) => new Uint8Array([ ...new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8X"), ...[10, 0, 0, 0], flags, ]); assertEquals(scalableImage(webp(0x02)), null); // A reader without WebP gets a PNG of what may be transparent, a JPEG of // the rest. const still = { orientation: 1, animated: false }; assertEquals(scalableImage(webp(0x10)), { ...still, compat: "png" }); assertEquals( scalableImage(new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8 ")), { ...still, compat: "png" }, ); assertEquals(scalableImage(PNG), { ...still, compat: "png" }); assertEquals(scalableImage(jpegWithOrientation(1, true)), { ...still, compat: "jpg", }); assertEquals(scalableImage(new TextEncoder().encode("BM-a-bitmap")), { ...still, compat: "jpg", }); // Only a GIF's frames are an animation to keep. assertEquals(scalableImage(new TextEncoder().encode("GIF89a")), { orientation: 1, animated: true, compat: "png", }); assertEquals(scalableImage(new TextEncoder().encode("")), null); assertEquals(scalableImage(new Uint8Array()), null); });