forked from EduCraft/curriculum-project-hub
8a81c60ea5
pbank materialize previously shelled out to `unzip` and soft-failed when the binary was missing, so agents only saw titles. Read zip entries with Node zlib (store/deflate) and write under workspace .pbank-sources.
57 lines
2.6 KiB
TypeScript
57 lines
2.6 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { inflateRawSync } from "node:zlib";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
/** Mirrors hub/src/capability/pbank.ts zip reader contracts. */
|
|
function listZipEntries(buffer: Buffer) {
|
|
let eocd = -1;
|
|
const minEocd = Math.max(0, buffer.length - (22 + 0xffff));
|
|
for (let i = buffer.length - 22; i >= minEocd; i -= 1) {
|
|
if (buffer.readUInt32LE(i) === 0x06054b50) {
|
|
eocd = i;
|
|
break;
|
|
}
|
|
}
|
|
if (eocd < 0) throw new Error("missing eocd");
|
|
const totalEntries = buffer.readUInt16LE(eocd + 10);
|
|
const centralOffset = buffer.readUInt32LE(eocd + 16);
|
|
const entries: Array<{ name: string; method: number; compressedSize: number; uncompressedSize: number; localHeaderOffset: number }> = [];
|
|
let offset = centralOffset;
|
|
for (let i = 0; i < totalEntries; i += 1) {
|
|
const method = buffer.readUInt16LE(offset + 10);
|
|
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
const uncompressedSize = buffer.readUInt32LE(offset + 24);
|
|
const nameLen = buffer.readUInt16LE(offset + 28);
|
|
const extraLen = buffer.readUInt16LE(offset + 30);
|
|
const commentLen = buffer.readUInt16LE(offset + 32);
|
|
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
const nameStart = offset + 46;
|
|
const name = buffer.subarray(nameStart, nameStart + nameLen).toString("utf8");
|
|
entries.push({ name, method, compressedSize, uncompressedSize, localHeaderOffset });
|
|
offset = nameStart + nameLen + extraLen + commentLen;
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function inflateZipEntry(buffer: Buffer, entry: { method: number; compressedSize: number; uncompressedSize: number; localHeaderOffset: number }) {
|
|
const local = entry.localHeaderOffset;
|
|
const nameLen = buffer.readUInt16LE(local + 26);
|
|
const extraLen = buffer.readUInt16LE(local + 28);
|
|
const dataStart = local + 30 + nameLen + extraLen;
|
|
const compressed = buffer.subarray(dataStart, dataStart + entry.compressedSize);
|
|
if (entry.method === 0) return Buffer.from(compressed);
|
|
if (entry.method === 8) return Buffer.from(inflateRawSync(compressed));
|
|
throw new Error(`method ${entry.method}`);
|
|
}
|
|
|
|
describe("pbank zip reader contract", () => {
|
|
it("lists and inflates deflated entries without host unzip", async () => {
|
|
const zip = await readFile(new URL("./fixtures/pbank-mini.zip", import.meta.url));
|
|
const entries = listZipEntries(zip);
|
|
expect(entries.map((e) => e.name).sort()).toEqual(["fig/a.png", "hello.txt"]);
|
|
const hello = entries.find((e) => e.name === "hello.txt")!;
|
|
const text = inflateZipEntry(zip, hello).toString("utf8");
|
|
expect(text).toBe("hello pbank\n");
|
|
});
|
|
});
|