134 lines
4.0 KiB
TypeScript
134 lines
4.0 KiB
TypeScript
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;
|
|
}
|