import { describe, expect, it } from 'vitest'; import { DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS, inspectBrowserImage, } from './image-safety.js'; describe('browser image safety inspection', () => { it('accepts bounded PNG and JPEG dimensions', () => { expect(inspectBrowserImage(png(640, 480))).toMatchObject({ format: 'png', extension: 'png', mediaType: 'image/png', width: 640, height: 480, pixels: 307_200, browserRenderable: true, }); expect(inspectBrowserImage(jpeg(1_920, 1_080))).toMatchObject({ format: 'jpeg', extension: 'jpg', mediaType: 'image/jpeg', width: 1_920, height: 1_080, pixels: 2_073_600, browserRenderable: true, }); }); it('rejects huge PNG and JPEG dimensions without losing their type', () => { expect(inspectBrowserImage(png(0xffff_ffff, 1))).toMatchObject({ format: 'png', mediaType: 'image/png', width: 0xffff_ffff, height: 1, browserRenderable: false, rejectionReason: 'dimension-limit', }); expect(inspectBrowserImage(jpeg(0xffff, 0xffff))).toMatchObject({ format: 'jpeg', mediaType: 'image/jpeg', width: 0xffff, height: 0xffff, browserRenderable: false, rejectionReason: 'dimension-limit', }); }); it('enforces decoded pixel and compressed-byte limits independently', () => { expect( inspectBrowserImage(png(8_192, 8_193), { ...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS, maxDimension: 10_000, }) ).toMatchObject({ pixels: 67_117_056, browserRenderable: false, rejectionReason: 'pixel-limit', }); expect( inspectBrowserImage(png(1, 1), { ...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS, maxBytes: 32, }) ).toMatchObject({ width: 1, height: 1, browserRenderable: false, rejectionReason: 'byte-limit', }); }); it('rejects malformed PNG IHDR and JPEG marker streams', () => { const malformedPng = png(16, 16); malformedPng[11] = 12; const malformedJpeg = jpeg(16, 16); malformedJpeg[4] = 0; malformedJpeg[5] = 1; expect(inspectBrowserImage(malformedPng)).toMatchObject({ format: 'png', browserRenderable: false, rejectionReason: 'invalid-header', }); expect(inspectBrowserImage(malformedJpeg)).toMatchObject({ format: 'jpeg', browserRenderable: false, rejectionReason: 'invalid-header', }); expect(inspectBrowserImage(Uint8Array.of(0xff, 0xd8, 0xff))).toMatchObject({ format: 'jpeg', browserRenderable: false, rejectionReason: 'invalid-header', }); }); it('caps a JPEG marker walk that never reaches a frame header', () => { const bytes = new Uint8Array(2 + 5_000 * 2); bytes.set([0xff, 0xd8], 0); for (let offset = 2; offset < bytes.length; offset += 2) { bytes[offset] = 0xff; bytes[offset + 1] = 0xd0; } expect(inspectBrowserImage(bytes)).toMatchObject({ format: 'jpeg', browserRenderable: false, rejectionReason: 'invalid-header', }); }); }); function png(width: number, height: number): Uint8Array { const bytes = new Uint8Array(33); bytes.set([0x89, 0x50, 0x4e, 0x47, 13, 10, 26, 10], 0); writeU32Be(bytes, 8, 13); bytes.set([0x49, 0x48, 0x44, 0x52], 12); writeU32Be(bytes, 16, width); writeU32Be(bytes, 20, height); bytes.set([8, 6, 0, 0, 0], 24); return bytes; } function jpeg(width: number, height: number): Uint8Array { return Uint8Array.of( 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x02, 0xff, 0xc0, 0x00, 0x11, 0x08, (height >>> 8) & 0xff, height & 0xff, (width >>> 8) & 0xff, width & 0xff, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00 ); } function writeU32Be(bytes: Uint8Array, offset: number, value: number): void { bytes[offset] = (value >>> 24) & 0xff; bytes[offset + 1] = (value >>> 16) & 0xff; bytes[offset + 2] = (value >>> 8) & 0xff; bytes[offset + 3] = value & 0xff; }