120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
// @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`);
|
|
}
|