Files
reverse-engineering-anything/src/artifacts/ZipArtifactReader.test.ts
T
5fa4defcbc feat(artifacts): inspect and extract Godot PCK with retained facts (#3634)
* feat(artifacts): read Godot PCK packs

Add a Godot Engine PCK artifact reader for standalone packs and packs
embedded behind an executable trailer (formats 2 and 3). Members are
uncompressed byte ranges verified against their declared MD5 on read.

Encrypted directories, sparse bundles, patch removal entries, unknown
flags and format 1 fail closed as unavailable; encrypted members are
reported without being read. inspect-artifact and extract-artifact
recognize standalone packs by their GDPC magic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(artifacts): bound Godot PCK metadata and own member streams

Cap member paths at 4096 bytes and the retained directory at 64 MiB
before allocation. Read members with positional chunk reads through
pipeline so cancellation is typed, abandoned consumers release their
source, and the shared descriptor stays reader-owned. Retain the root
descriptor for godot-pck inventory, as for ZIP and universal Mach-O.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(artifacts): complete Godot PCK facts, integrity and selected views

* fix(artifacts): preserve atomic-root inventory Evidence

* fix(artifacts): bound aggregate PCK member expansion

---------

Co-authored-by: Samucahub <151155051+Samucahub@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-20 09:53:53 +08:00

207 lines
8.2 KiB
TypeScript

import { writeOrderedZip } from "../../tests/fixtures/artifactEntryOrder.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
TextReader,
Uint8ArrayReader,
Uint8ArrayWriter,
ZipWriter,
} from "@zip.js/zip.js";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { ZipArtifactReader } from "./ZipArtifactReader.js";
import { MINIMUM_ARTIFACT_EXPANDED_BYTES } from "../domain/artifactExpansionBudget.js";
/** Small real ZIP carriers with forged sizes; metadata-only controls, never decompressed. */
const writeZipWithDeclaredSize = async (
directory: string,
declaredBytes: readonly number[],
): Promise<string> => {
const path = join(directory, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
for (const [index] of declaredBytes.entries())
await writer.add(
`member-${index}.bin`,
new TextReader("real member bytes"),
);
await writeFile(path, await writer.close());
const bytes = await readFile(path);
let offset = 0;
for (const declared of declaredBytes) {
const centralDirectory = bytes.indexOf(
Buffer.from([0x50, 0x4b, 0x01, 0x02]),
offset,
);
if (centralDirectory < 0)
throw new Error("Expected a ZIP central directory");
// Metadata-only controls: these declarations do not describe valid member streams.
bytes.writeUInt32LE(declared, centralDirectory + 24);
offset = centralDirectory + 46;
}
await writeFile(path, bytes);
return path;
};
const enumerate = async (reader: ZipArtifactReader): Promise<number> => {
let files = 0;
for await (const entry of reader.entries())
if (entry.kind === "file") files += 1;
return files;
};
describe("ZipArtifactReader decompression budget", () => {
it("refuses an archive whose entries declare more than its size can justify", async () => {
const directory = await createTestTempDirectory("rea-zip-budget-bomb-");
// A ~160-byte archive cannot declare 2 GiB of members without being a bomb.
const path = await writeZipWithDeclaredSize(directory, [2_147_483_647]);
const reader = new ZipArtifactReader(path, "zip");
try {
await expect(enumerate(reader)).rejects.toMatchObject({
name: "ArtifactReaderFailure",
reason: "limit",
});
} finally {
await reader.close();
}
});
it("charges all declarations in one traversal before yielding an over-budget member", async () => {
const directory = await createTestTempDirectory("rea-zip-budget-total-");
const half = MINIMUM_ARTIFACT_EXPANDED_BYTES / 2;
const path = await writeZipWithDeclaredSize(directory, [half, half + 1]);
const reader = new ZipArtifactReader(path, "zip");
const iterator = reader.entries()[Symbol.asyncIterator]();
try {
expect((await iterator.next()).value?.declaredSize).toBe(half);
await expect(iterator.next()).rejects.toMatchObject({ reason: "limit" });
} finally {
await iterator.return?.();
await reader.close();
}
});
it("keeps repeated and interleaved metadata traversals independently charged", async () => {
const directory = await createTestTempDirectory(
"rea-zip-budget-iterators-",
);
const path = await writeZipWithDeclaredSize(directory, [
MINIMUM_ARTIFACT_EXPANDED_BYTES / 2,
MINIMUM_ARTIFACT_EXPANDED_BYTES / 2,
]);
const reader = new ZipArtifactReader(path, "zip");
const first = reader.entries()[Symbol.asyncIterator]();
const second = reader.entries()[Symbol.asyncIterator]();
try {
expect((await first.next()).done).toBe(false);
expect((await second.next()).done).toBe(false);
expect((await first.next()).done).toBe(false);
expect((await second.next()).done).toBe(false);
expect((await first.next()).done).toBe(true);
expect((await second.next()).done).toBe(true);
expect(await enumerate(reader)).toBe(2);
} finally {
await Promise.all([first.return?.(), second.return?.()]);
await reader.close();
}
});
it("streams a valid small member through open", async () => {
const directory = await createTestTempDirectory("rea-zip-budget-valid-");
const path = join(directory, "valid.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("member.bin", new TextReader("valid member bytes"));
await writeFile(path, await writer.close());
const reader = new ZipArtifactReader(path, "zip");
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries[0];
if (entry === undefined) throw new Error("Expected a ZIP member");
const chunks: Buffer[] = [];
for await (const chunk of await reader.open(entry))
chunks.push(Buffer.from(chunk));
expect(Buffer.concat(chunks).toString()).toBe("valid member bytes");
} finally {
await reader.close();
}
});
it("rejects under-declared member output before delivering its complete actual bytes", async () => {
const directory = await createTestTempDirectory("rea-zip-underdeclared-");
const path = join(directory, "underdeclared.zip");
const content = new Uint8Array(128 * 1024);
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("member.bin", new Uint8ArrayReader(content), {
dataDescriptor: false,
bufferedWrite: true,
});
const bytes = Buffer.from(await writer.close());
const local = bytes.indexOf(Buffer.from([0x50, 0x4b, 0x03, 0x04]));
const central = bytes.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
if (local < 0 || central < 0) throw new Error("Expected ZIP entry headers");
// Both headers agree and no descriptor remains: rejection must enforce
// actual decompressed output, not merely detect conflicting metadata.
expect(bytes.readUInt16LE(local + 6) & 8).toBe(0);
expect(bytes.readUInt16LE(local + 8)).toBe(8);
expect(bytes.readUInt32LE(local + 22)).toBe(content.length);
expect(bytes.readUInt32LE(central + 24)).toBe(content.length);
bytes.writeUInt32LE(1, local + 22);
bytes.writeUInt32LE(1, central + 24);
await writeFile(path, bytes);
const reader = new ZipArtifactReader(path, "zip");
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries[0];
if (entry === undefined) throw new Error("Expected a ZIP member");
expect(entry.declaredSize).toBe(1);
let deliveredBytes = 0;
await expect(
(async () => {
for await (const chunk of await reader.open(entry))
deliveredBytes += Buffer.from(chunk).length;
})(),
).rejects.toThrow("Invalid uncompressed size");
expect(deliveredBytes).toBeLessThan(content.length);
} finally {
await reader.close();
}
});
});
it.each([null, { caller: "opaque ZIP interruption" }])(
"preserves the exact ZIP cancellation reason at enumeration and open: %j",
async (reason) => {
const root = await createTestTempDirectory("rea-zip-cancellation-");
const path = join(root, "fixture.zip");
await writeOrderedZip(path, ["member.txt"]);
const reader = new ZipArtifactReader(path, "zip");
const caller = new AbortController();
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries[0];
if (entry === undefined) throw new Error("Expected real ZIP member");
caller.abort(reason);
for (const run of [
() => reader.entries(caller.signal)[Symbol.asyncIterator]().next(),
async () => reader.open(entry, caller.signal),
]) {
const error: unknown = await run().catch((cause: unknown) => cause);
expect(error).toBeInstanceOf(ArtifactReaderFailure);
if (!(error instanceof ArtifactReaderFailure))
throw new Error("Expected ZIP cancellation");
expect(error.reason).toBe("cancelled");
expect(error.cancellation?.reason).toBe(reason);
expect(error.cause).toBe(reason);
}
} finally {
await reader.close();
}
},
);