import { lstat, mkdir, symlink, 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 { classifyAndHashRoot, inventoryArtifact, } from "../../fixtures/artifactInventory.js"; import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js"; import { artifactOccurrenceAt } from "../../fixtures/artifactEntryOrder.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { asarHeaderBytes } from "../../fixtures/asarHeader.js"; describe("ZIP signature detection", () => { it.each([ { signature: [0x50, 0x4b, 0x05, 0x06], archive: true }, { signature: [0x50, 0x4b, 0x03, 0x06], archive: false }, { signature: [0x50, 0x4b, 0x03, 0x04], archive: true }, { signature: [0x50, 0x4b, 0x07, 0x08], archive: true }, ] as const)( "routes representative ZIP signature $signature through inventory and target parsing", async ({ signature, archive }) => { const root = await createTestTempDirectory("rea-zip-signature-"); const path = join(root, "input"); await writeFile(path, Buffer.from(signature)); expect( (await classifyAndHashRoot(path, false, await lstat(path))).format, ).toBe(archive ? "zip" : "file"); const target = await parseBinaryTarget(path); if (archive) expect(target).toMatchObject({ ok: true, value: { format: "zip" } }); else expect(target).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" }, }); }, ); it("uses ZIP magic before the filename suffix", async () => { const root = await createTestTempDirectory("rea-zip-suffix-"); const apk = join(root, "renamed.apk"); const program = join(root, "program.apk"); await writeFile(apk, Buffer.from([0x50, 0x4b, 0x03, 0x04])); await writeFile(program, Buffer.from([0x7f, 0x45, 0x4c, 0x46])); expect( (await classifyAndHashRoot(apk, false, await lstat(apk))).format, ).toBe("apk"); expect( (await classifyAndHashRoot(program, false, await lstat(program))).format, ).toBe("elf"); }); it("requires container magic for asar, pkg, and dmg suffixes", async () => { const root = await createTestTempDirectory("rea-container-magic-"); const asar = join(root, "app.asar"); const textAsar = join(root, "notes.asar"); const pkg = join(root, "install.pkg"); const textPkg = join(root, "notes.pkg"); const dmg = join(root, "image.dmg"); const textDmg = join(root, "notes.dmg"); const asarBytes = asarHeaderBytes(JSON.stringify({ files: {} })); await writeFile(asar, asarBytes); await writeFile(textAsar, Buffer.from("not an asar")); await writeFile(pkg, Buffer.from("xar!")); await writeFile(textPkg, Buffer.from("not a pkg")); const image = Buffer.alloc(512); image.write("koly", 0, "ascii"); await writeFile(dmg, image); await writeFile(textDmg, Buffer.from("not a dmg")); expect( (await classifyAndHashRoot(asar, false, await lstat(asar))).format, ).toBe("asar"); expect( (await classifyAndHashRoot(textAsar, false, await lstat(textAsar))) .format, ).toBe("file"); // A valid prefix cannot admit extents outside the observed container. await writeFile(asar, asarBytes.subarray(0, -1)); expect( (await classifyAndHashRoot(asar, false, await lstat(asar))).format, ).toBe("file"); expect( (await classifyAndHashRoot(pkg, false, await lstat(pkg))).format, ).toBe("pkg"); expect( (await classifyAndHashRoot(textPkg, false, await lstat(textPkg))).format, ).toBe("file"); expect( (await classifyAndHashRoot(dmg, false, await lstat(dmg))).format, ).toBe("dmg"); expect( (await classifyAndHashRoot(textDmg, false, await lstat(textDmg))).format, ).toBe("file"); }); it.each([[], [0x50], [0x50, 0x4b], [0x50, 0x4b, 0x03]])( "rejects an incomplete signature %j", async (...bytes) => { const root = await createTestTempDirectory("rea-zip-short-"); const path = join(root, "input"); await writeFile(path, Buffer.from(bytes)); expect( (await classifyAndHashRoot(path, false, await lstat(path))).format, ).toBe("file"); expect((await parseBinaryTarget(path)).ok).toBe(false); }, ); }); describe("nested container family", () => { const plainZip = async (): Promise => { const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("readme.txt", new TextReader("hello\n")); return writer.close(); }; it("takes ZIP families from member bytes, not member suffixes", async () => { const root = await createTestTempDirectory("rea-nested-zip-family-"); const path = join(root, "outer.zip"); const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("inner-text.apk", new TextReader("just some text\n")); await writer.add("inner-real.apk", new Uint8ArrayReader(await plainZip())); await writer.add("renamed.bin", new Uint8ArrayReader(await plainZip())); await writer.add("locked.ipa", new TextReader("encrypted bytes"), { password: "fixture-only", }); await writeFile(path, await writer.close()); const observed = await inventoryArtifact(path); expect(artifactOccurrenceAt(observed, "inner-text.apk")).toMatchObject({ artifact_kind: "resource", artifact_format: "file", limitations: [], }); expect(artifactOccurrenceAt(observed, "inner-real.apk")).toMatchObject({ artifact_kind: "container", artifact_format: "apk", }); expect(artifactOccurrenceAt(observed, "renamed.bin")).toMatchObject({ artifact_kind: "container", artifact_format: "zip", }); expect(artifactOccurrenceAt(observed, "locked.ipa")).toMatchObject({ artifact_kind: "unknown", artifact_format: "unknown", hash_status: "unavailable", limitations: [ "The path suffix suggests IPA, but these bytes were not read, so the container family is unknown.", ], }); }); it.skipIf(process.platform === "win32")( "does not name a container family for a symlink it did not follow", async () => { const root = await createTestTempDirectory("rea-nested-symlink-family-"); const directory = join(root, "application"); await mkdir(directory); await writeFile(join(directory, "real.apk"), await plainZip()); await symlink("real.apk", join(directory, "link.apk")); const observed = await inventoryArtifact(directory); expect(artifactOccurrenceAt(observed, "link.apk")).toMatchObject({ entry_kind: "symlink", artifact_kind: "unknown", artifact_format: "unknown", hash_status: "not-hashed", limitations: [ "Symlink target was not followed or disclosed.", "The path suffix suggests APK, but these bytes were not read, so the container family is unknown.", ], }); expect(artifactOccurrenceAt(observed, "real.apk")).toMatchObject({ artifact_kind: "container", artifact_format: "apk", }); }, ); }); const SCRIPT_STUB = Buffer.from("#!/bin/sh\nexit 0\n"); const memberZip = async (): Promise => { const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("a.txt", new TextReader("hello world\n")); await writer.add("d/b.txt", new TextReader("x\n")); return Buffer.from(await writer.close()); }; /** Prepend bytes; optionally shift stored offsets as `zip -A` does. */ const withLeadingBytes = ( zip: Buffer, leading: Buffer, adjustOffsets: boolean, ): Buffer => { const bytes = Buffer.concat([leading, zip]); if (!adjustOffsets) return bytes; const end = bytes.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06])); const directory = bytes.readUInt32LE(end + 16) + leading.length; bytes.writeUInt32LE(directory, end + 16); for (let at = directory; at < end;) { bytes.writeUInt32LE(bytes.readUInt32LE(at + 42) + leading.length, at + 42); at += 46 + bytes.readUInt16LE(at + 28) + bytes.readUInt16LE(at + 30) + bytes.readUInt16LE(at + 32); } return bytes; }; /** Members by content-derived identity and verification state. */ const members = async (path: string) => { const inventory = await inventoryArtifact(path); return ["a.txt", "d/b.txt"].map((member) => { const occurrence = artifactOccurrenceAt(inventory, member); return [member, occurrence.hash_status, occurrence.artifact_id]; }); }; describe("ZIP archives after leading bytes", () => { it.each([ { style: "offsets that include the leading bytes", adjust: true }, { style: "offsets written before the bytes were prepended", adjust: false }, ])( "admits a ZIP after an unclaimed script stub with $style", async ({ adjust }) => { const root = await createTestTempDirectory("rea-zip-leading-"); const plain = join(root, "plain.zip"); const prefixed = join(root, "installer.bin"); const zip = await memberZip(); await writeFile(plain, zip); await writeFile(prefixed, withLeadingBytes(zip, SCRIPT_STUB, adjust)); expect( (await classifyAndHashRoot(prefixed, false, await lstat(prefixed))) .format, ).toBe("zip"); expect(await parseBinaryTarget(prefixed)).toMatchObject({ ok: true, value: { kind: "archive", format: "zip" }, }); // Members are the same verified bytes as in the plain archive. const expected = await members(plain); expect(expected.map(([, status]) => status)).toEqual([ "verified", "verified", ]); expect(await members(prefixed)).toEqual(expected); }, ); it("keeps the root App Pack family for a ZIP after a script stub", async () => { const root = await createTestTempDirectory("rea-zip-leading-app-pack-"); const path = join(root, "Example.app"); await writeFile( path, withLeadingBytes(await memberZip(), SCRIPT_STUB, false), ); expect( (await classifyAndHashRoot(path, false, await lstat(path))).format, ).toBe("app-pack"); expect(await parseBinaryTarget(path)).toMatchObject({ ok: true, value: { kind: "archive", format: "app-pack" }, }); }); it.each([ { family: "ELF", leading: Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 1, 1]) }, { family: "MZ", leading: Buffer.concat([Buffer.from("MZ"), Buffer.alloc(62)]), }, { family: "Mach-O", leading: Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0, 0, 0, 0]), }, ])( "keeps a $family-led file with an appended ZIP out of the ZIP route", async ({ leading }) => { const root = await createTestTempDirectory("rea-zip-claimed-lead-"); const path = join(root, "program"); await writeFile(path, withLeadingBytes(await memberZip(), leading, true)); expect( (await classifyAndHashRoot(path, false, await lstat(path))).format, ).not.toBe("zip"); expect(await parseBinaryTarget(path)).not.toMatchObject({ ok: true, value: { format: "zip" }, }); }, ); it.each([ { shape: "bytes after the end record", mutate: (bytes: Buffer) => Buffer.concat([bytes, Buffer.from("junk")]), }, { shape: "a corrupt central directory header", mutate: (bytes: Buffer) => { bytes.write("XXXX", bytes.indexOf(Buffer.from([0x50, 0x4b, 1, 2]))); return bytes; }, }, { shape: "a Zip64 directory offset", mutate: (bytes: Buffer) => { const end = bytes.lastIndexOf(Buffer.from([0x50, 0x4b, 5, 6])); bytes.writeUInt32LE(0xffffffff, end + 16); return bytes; }, }, { shape: "an empty archive record", mutate: (bytes: Buffer) => { const end = bytes.lastIndexOf(Buffer.from([0x50, 0x4b, 5, 6])); bytes.writeUInt16LE(0, end + 8); bytes.writeUInt16LE(0, end + 10); return bytes; }, }, ])("does not locate a trailing ZIP with $shape", async ({ mutate }) => { const root = await createTestTempDirectory("rea-zip-leading-negative-"); const path = join(root, "input.bin"); await writeFile( path, mutate(withLeadingBytes(await memberZip(), SCRIPT_STUB, true)), ); expect( (await classifyAndHashRoot(path, false, await lstat(path))).format, ).toBe("file"); expect(await parseBinaryTarget(path)).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" }, }); }); }); describe("package signature byte identity", () => { it.each([ { extension: "pkg", signature: "xar!", offset: 0, size: 4 }, { extension: "dmg", signature: "koly", offset: 0, size: 512 }, ])( "rejects high-bit aliases of $extension magic at both admission boundaries", async ({ extension, signature, offset, size }) => { const root = await createTestTempDirectory("rea-package-magic-"); const path = join(root, `input.${extension}`); const bytes = Buffer.alloc(size); bytes.write(signature, offset, "ascii"); await writeFile(path, bytes); expect( (await classifyAndHashRoot(path, false, await lstat(path))).format, ).toBe(extension); expect(await parseBinaryTarget(path)).toMatchObject({ ok: true, value: { format: extension }, }); for (let index = 0; index < signature.length; index += 1) { const changed = Buffer.from(bytes); changed[offset + index] = changed[offset + index]! | 0x80; await writeFile(path, changed); expect( (await classifyAndHashRoot(path, false, await lstat(path))).format, ).toBe("file"); expect(await parseBinaryTarget(path)).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" }, }); } }, ); });