import { describe, expect, it } from "vitest"; import { inspectImageFile, inspectImageHeader } from "../../src/image/headers"; import { IMAGE_LIMITS } from "../../src/image/limits"; import { jpegFixture, pngFixture, webpFixture } from "../fixtures/images"; describe("bounded image header inspection", () => { it("reads PNG dimensions, alpha, metadata and EXIF orientation", () => { const inspected = inspectImageHeader( pngFixture(640, 480, { exif: true, text: true }), ); expect(inspected).toMatchObject({ format: "png", width: 640, height: 480, orientedWidth: 480, orientedHeight: 640, orientation: 6, hasAlpha: true, animated: false, metadata: { exif: true, text: true }, }); }); it("detects APNG before decode and refuses to flatten it", async () => { const bytes = pngFixture(32, 24, { animated: true }); expect(inspectImageHeader(bytes).animated).toBe(true); const file = new File([arrayBuffer(bytes)], "animated.png", { type: "image/png", }); await expect(inspectImageFile(file)).rejects.toMatchObject({ code: "STATIC_ONLY", }); }); it("finds APNG control chunks beyond the bounded metadata window", async () => { const bytes = pngFixture(32, 24, { animated: true, paddingBytes: IMAGE_LIMITS.maxHeaderBytes + 32, }); expect( inspectImageHeader(bytes.subarray(0, IMAGE_LIMITS.maxHeaderBytes)) .animated, ).toBe(false); await expect( inspectImageFile(new File([arrayBuffer(bytes)], "late-animation.png")), ).rejects.toMatchObject({ code: "STATIC_ONLY" }); }); it("reads JPEG frame dimensions and orientation", () => { const inspected = inspectImageHeader( jpegFixture(4032, 3024, { orientation: 8 }), ); expect(inspected).toMatchObject({ format: "jpeg", width: 4032, height: 3024, orientedWidth: 3024, orientedHeight: 4032, orientation: 8, hasAlpha: false, }); }); it("flags recognized JPEG multi-picture containers", async () => { const bytes = jpegFixture(64, 48, { multiPicture: true }); expect(inspectImageHeader(bytes).metadata.multiPicture).toBe(true); await expect( inspectImageFile(new File([arrayBuffer(bytes)], "multi.jpg")), ).rejects.toMatchObject({ code: "STATIC_ONLY", }); }); it("reads WebP extended dimensions and feature flags", () => { const inspected = inspectImageHeader( webpFixture(1920, 1080, { alpha: true, metadata: true }), ); expect(inspected).toMatchObject({ format: "webp", width: 1920, height: 1080, animated: false, hasAlpha: true, metadata: { exif: true, xmp: true, icc: true }, }); }); it("finds WebP animation chunks beyond the metadata window", async () => { const bytes = webpFixture(32, 24, { animationChunk: true, paddingBytes: IMAGE_LIMITS.maxHeaderBytes + 32, }); expect( inspectImageHeader( bytes.subarray(0, IMAGE_LIMITS.maxHeaderBytes), bytes.byteLength, ).animated, ).toBe(false); await expect( inspectImageFile(new File([arrayBuffer(bytes)], "late-animation.webp")), ).rejects.toMatchObject({ code: "STATIC_ONLY" }); }); it("rejects unsafe declared dimensions before decode", async () => { const file = new File( [arrayBuffer(pngFixture(32_769, 1))], "too-wide.png", { type: "image/png", }, ); await expect(inspectImageFile(file)).rejects.toMatchObject({ code: "PIXEL_LIMIT", }); }); it("fails closed for unsupported and truncated input", () => { expect(() => inspectImageHeader(Uint8Array.of(0, 1, 2, 3))).toThrow( /Only static JPEG/u, ); expect(() => inspectImageHeader( Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a), ), ).toThrow(/PNG header is truncated/u); }); }); function arrayBuffer(bytes: Uint8Array): ArrayBuffer { const buffer = new ArrayBuffer(bytes.byteLength); new Uint8Array(buffer).set(bytes); return buffer; }