/** * Tests for the JPEG EXIF scan behind `quak backup-metadata --exif`. * * The originals come from users' libraries, so a truncated or corrupt JPEG * must neither hang the scan nor throw out of it, and a malformed file must be * told apart from one that simply has no EXIF: the record carries the reason in * `exifError`. Each input below is a short hand-built byte array. */ import { describe, expect, it } from "vitest"; import { extractExifFromJpeg, extractImageMetadata, } from "../../src/metadata-backup.js"; const SOI = [0xff, 0xd8]; // start of image const SOS = [0xff, 0xda, 0x00, 0x02]; // start of scan, where the scan stops const EXIF_HEADER = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00]; // "Exif\0\0" // A big-endian TIFF block with one IFD entry: Orientation (0x0112), SHORT, 6. const TIFF_ORIENTATION_6 = [ 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; // An APP1 segment whose length field matches its data. const app1 = (data: number[]): number[] => { const len = data.length + 2; return [0xff, 0xe1, len >> 8, len & 0xff, ...data]; }; const bytes = (...parts: number[][]): Uint8Array => new Uint8Array(parts.flat()); describe("extractExifFromJpeg", () => { it("returns the EXIF segment of a valid JPEG", () => { const data = [...EXIF_HEADER, ...TIFF_ORIENTATION_6]; const scan = extractExifFromJpeg(bytes(SOI, app1(data), SOS)); expect(scan.error).toBeUndefined(); expect([...scan.exif!]).toEqual(data); }); it("returns nothing for a file that is not a JPEG", () => { const png = bytes([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); expect(extractExifFromJpeg(png)).toEqual({}); }); it("returns nothing for a JPEG without EXIF", () => { const app0 = [0xff, 0xe0, 0x00, 0x04, 0x00, 0x00]; expect(extractExifFromJpeg(bytes(SOI, app0, SOS))).toEqual({}); }); it("ignores an APP1 segment too short to hold the Exif header", () => { // Length 7 leaves room for "Exif\0" only. Without the length check // the scan compared the header against bytes past the segment. const short = app1(EXIF_HEADER.slice(0, 5)); expect(extractExifFromJpeg(bytes(SOI, short, SOS))).toEqual({}); }); it("reports a JPEG truncated inside a segment header", () => { const scan = extractExifFromJpeg(bytes(SOI, [0xff, 0xe1, 0x00])); expect(scan.exif).toBeUndefined(); expect(scan.error).toMatch(/truncated segment length/); }); it("reports a JPEG that ends before the image data", () => { const app0 = [0xff, 0xe0, 0x00, 0x04, 0x00, 0x00]; const scan = extractExifFromJpeg(bytes(SOI, app0)); expect(scan.error).toMatch(/ends before the image data/); }); it("stops on a zero-length segment instead of looping", () => { // A length of 0 would otherwise step the scan by 2 bytes at a time // through the rest of the file, reading garbage as markers. const zero = [0xff, 0xe0, 0x00, 0x00]; const scan = extractExifFromJpeg( bytes(SOI, zero, zero, zero, zero, SOS), ); expect(scan.error).toMatch(/segment length 0 at byte 2 is too small/); }); it("stops on a segment length of 1", () => { const scan = extractExifFromJpeg( bytes(SOI, [0xff, 0xe0, 0x00, 0x01], SOS), ); expect(scan.error).toMatch(/segment length 1 at byte 2 is too small/); }); it("reports a segment length that runs past the end of the file", () => { // APP1 claims 0x4000 bytes but only the "Exif\0\0" header follows. const scan = extractExifFromJpeg( bytes(SOI, [0xff, 0xe1, 0x40, 0x00], EXIF_HEADER), ); expect(scan.exif).toBeUndefined(); expect(scan.error).toMatch(/runs past the end of the file/); }); }); describe("extractImageMetadata", () => { it("parses EXIF from a valid JPEG", () => { const meta = extractImageMetadata( bytes(SOI, app1([...EXIF_HEADER, ...TIFF_ORIENTATION_6]), SOS), ); expect(meta?.exifError).toBeUndefined(); expect(meta?.exif).toMatchObject({ Image: { Orientation: 6 } }); }); it("returns nothing for a file that is not a JPEG", () => { const text = new TextEncoder().encode("just some text, not an image"); expect(extractImageMetadata(text)).toBeUndefined(); }); it("records the reason when the JPEG is malformed", () => { const meta = extractImageMetadata( bytes(SOI, [0xff, 0xe1, 0x40, 0x00], EXIF_HEADER), ); expect(meta?.exif).toBeUndefined(); expect(meta?.exifError).toMatch(/runs past the end of the file/); }); it("keeps the raw bytes and the reason when EXIF cannot be parsed", () => { const data = [...EXIF_HEADER, 0x58, 0x58]; const meta = extractImageMetadata(bytes(SOI, app1(data), SOS)); expect(meta?.exif).toBeUndefined(); expect(meta?.exifRaw).toBe(Buffer.from(data).toString("base64")); expect(meta?.exifError).toEqual(expect.any(String)); }); });