diff --git a/src/cli/unpack.ts b/src/cli/unpack.ts index 2bf78c2..a877213 100644 --- a/src/cli/unpack.ts +++ b/src/cli/unpack.ts @@ -93,6 +93,14 @@ export async function unpackExtension({ for (const relativePath in decompressed) { if (Object.prototype.hasOwnProperty.call(decompressed, relativePath)) { + // Skip explicit directory entries (keys ending in "/"). ZIP archives + // produced by most tooling (e.g. `zip -r`) include these with empty + // data; writing to a path with a trailing slash fails and would abort + // the whole unpack. Needed parent directories are created below. + if (relativePath.endsWith("/")) { + continue; + } + const data = decompressed[relativePath]; const fullPath = join(finalOutputDir, relativePath); diff --git a/test/unpack.test.ts b/test/unpack.test.ts new file mode 100644 index 0000000..2aa2513 --- /dev/null +++ b/test/unpack.test.ts @@ -0,0 +1,41 @@ +import fs from "node:fs"; +import { join } from "node:path"; + +import { zipSync } from "fflate"; + +import { unpackExtension } from "../src/cli/unpack"; + +describe("unpackExtension", () => { + const tmpRoot = join(__dirname, "temp-unpack-dir-entries"); + const mcpbPath = join(tmpRoot, "demo.mcpb"); + const outDir = join(tmpRoot, "out"); + + beforeEach(() => { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + fs.mkdirSync(tmpRoot, { recursive: true }); + }); + + afterAll(() => { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + }); + + it("unpacks archives that contain explicit directory entries", () => { + // Standard ZIP tooling emits directory entries (keys ending in "/"). + const archive = zipSync({ + "manifest.json": new TextEncoder().encode("{}"), + "server/": new Uint8Array(0), + "server/index.js": new TextEncoder().encode("console.log('hi');"), + }); + fs.writeFileSync(mcpbPath, archive); + + const ok = unpackExtension({ mcpbPath, outputDir: outDir, silent: true }); + + return Promise.resolve(ok).then((result) => { + expect(result).toBe(true); + expect(fs.existsSync(join(outDir, "manifest.json"))).toBe(true); + expect(fs.existsSync(join(outDir, "server", "index.js"))).toBe(true); + // The directory entry must not be written as a file. + expect(fs.statSync(join(outDir, "server")).isDirectory()).toBe(true); + }); + }); +});