// @vitest-environment node import { File as NodeFile } from "node:buffer"; import { describe, expect, it } from "vitest"; import { previewEntry } from "../../src/archive/preview"; import type { ArchiveDocument, ArchiveEntryRecord, } from "../../src/archive/types"; describe("bounded previews", () => { it("returns a safe static PNG Blob only after dimension inspection", async () => { const png = Uint8Array.from( Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64", ), ); await expect(previewFixture("pixel.png", png)).resolves.toMatchObject({ kind: "image", mimeType: "image/png", width: 1, height: 1, }); }); it("refuses animated PNG previews before browser decoding", async () => { const png = Uint8Array.from( Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64", ), ); const idat = findChunk(png, "IDAT"); const actl = new Uint8Array([ 0, 0, 0, 8, 0x61, 0x63, 0x54, 0x4c, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, ]); const animated = new Uint8Array(png.length + actl.length); animated.set(png.subarray(0, idat), 0); animated.set(actl, idat); animated.set(png.subarray(idat), idat + actl.length); await expect(previewFixture("animated.png", animated)).rejects.toThrow( /Animated/iu, ); }); it("supports a bounded VP8X WebP inventory without interpreting active content", async () => { const webp = new Uint8Array(30); webp.set(new TextEncoder().encode("RIFF"), 0); new DataView(webp.buffer).setUint32(4, 22, true); webp.set(new TextEncoder().encode("WEBPVP8X"), 8); new DataView(webp.buffer).setUint32(16, 10, true); webp[24] = 2; webp[27] = 3; await expect(previewFixture("sample.webp", webp)).resolves.toMatchObject({ kind: "image", width: 3, height: 4, mimeType: "image/webp", }); }); it("shows nested archives as bytes without recursive expansion", async () => { const nested = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 1, 2, 3]); await expect(previewFixture("nested.zip", nested)).resolves.toMatchObject({ kind: "hex", note: expect.stringMatching(/Nested archive expansion/iu), }); }); }); function documentFor(path: string, payload: Uint8Array): ArchiveDocument { const entry: ArchiveEntryRecord = { id: "0", sourceIndex: 0, rawPath: path, path, collisionKey: path.toLocaleLowerCase("en-US"), kind: "file", size: payload.length, extractable: true, issues: [], dataOffset: 0, }; return { name: "fixture.gz", format: "gzip", source: new NodeFile( [Buffer.from(payload)], "fixture.gz", ) as unknown as File, sourceBytes: payload.length, entries: [entry], issues: [], expandedBytes: payload.length, compressedBytes: payload.length, zip64: false, payload, }; } function previewFixture(path: string, payload: Uint8Array) { const document = documentFor(path, payload); return previewEntry(document, document.entries[0]!); } function findChunk(bytes: Uint8Array, type: string): number { const expected = new TextEncoder().encode(type); for (let offset = 8; offset + 8 <= bytes.length;) { if (expected.every((value, index) => bytes[offset + 4 + index] === value)) return offset; const length = new DataView( bytes.buffer, bytes.byteOffset + offset, 4, ).getUint32(0); offset += 12 + length; } throw new Error(`${type} chunk not found`); }