Release Archive Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
// @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`);
|
||||
}
|
||||
Reference in New Issue
Block a user