Release Archive Tools 0.1.0

This commit is contained in:
2026-09-01 02:42:42 +02:00
commit a49ec6b17a
77 changed files with 11528 additions and 0 deletions
+119
View File
@@ -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`);
}