fix(hub): extract PBank zips in-process without host unzip

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.
This commit is contained in:
2026-07-23 21:05:15 +08:00
parent 00f7a8db39
commit 8a81c60ea5
3 changed files with 260 additions and 169 deletions
Binary file not shown.
+56
View File
@@ -0,0 +1,56 @@
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");
});
});