From 650873e772034a62df1623892be56bbafe74aa83 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:03:15 +0800 Subject: [PATCH] fix(artifacts): preserve mounted DMG traversal coverage --- src/artifacts/NativeDmgArtifactReader.ts | 1 + src/artifacts/NativeDmgProtocol.test.ts | 84 +++++++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/artifacts/NativeDmgArtifactReader.ts b/src/artifacts/NativeDmgArtifactReader.ts index cb9f47c583..a904b49e0f 100644 --- a/src/artifacts/NativeDmgArtifactReader.ts +++ b/src/artifacts/NativeDmgArtifactReader.ts @@ -202,6 +202,7 @@ export class NativeDmgArtifactReader implements ArtifactReader { const prefix = basename(this.path); const entries = directory .entries(signal, { + onCoverage: (coverage) => options?.onCoverage?.(coverage), onOmission: (omission) => options?.onOmission?.({ ...omission, diff --git a/src/artifacts/NativeDmgProtocol.test.ts b/src/artifacts/NativeDmgProtocol.test.ts index b1e5e3772a..e57132a987 100644 --- a/src/artifacts/NativeDmgProtocol.test.ts +++ b/src/artifacts/NativeDmgProtocol.test.ts @@ -1,13 +1,95 @@ import { ExecFileOutputCancelledError } from "../process/ExecFileOutput.js"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; import { build } from "plist"; -import { expect, it } from "vitest"; +import { expect, it, vi } from "vitest"; import { NativeDmgArtifactReader, type NativeDmgHost, } from "./NativeDmgArtifactReader.js"; import { translateArtifactFailure } from "./ArtifactOperationFailure.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { ArtifactReaderTraversal } from "./ArtifactReader.js"; +import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js"; + +it.skipIf(process.platform === "win32").each([false, true])( + "preserves mounted-directory coverage alongside readable DMG entries (unknown coverage: %s)", + async (unknownCoverage) => { + let mountPoint: string | undefined; + const coverage: ArtifactReaderTraversal[] = []; + const producerCoverage: ArtifactReaderTraversal = { + status: "unknown", + examinedEntries: 3, + limitations: [ + "Directory contains an entry name that is not valid UTF-8.", + ], + }; + // APFS rejects invalid UTF-8 basenames at creation. Inject the directory + // owner's completed observation while retaining its real entry traversal. + const entries = DirectoryArtifactReader.prototype.entries; + const traversal = vi + .spyOn(DirectoryArtifactReader.prototype, "entries") + .mockImplementation(async function* ( + this: DirectoryArtifactReader, + signal, + options, + ) { + yield* entries.call(this, signal, options); + if (unknownCoverage && typeof options !== "function") + options?.onCoverage?.(producerCoverage); + }); + const host: NativeDmgHost = { + run: async (args) => { + if (args[0] === "verify" || args[0] === "detach") + return { stdout: "", exitCode: 0 }; + if (args[0] === "attach") { + const root = args[args.indexOf("-mountroot") + 1]; + if (root === undefined) throw new Error("Missing mount root"); + mountPoint = join(root, "Volume"); + await mkdir(mountPoint); + await writeFile(join(mountPoint, "readable.txt"), "visible"); + } + if (args[0] === "info" || args[0] === "attach") { + const entities = + mountPoint === undefined + ? [] + : [{ "dev-entry": "/dev/disk800", "mount-point": mountPoint }]; + return { + stdout: build( + args[0] === "info" + ? { + images: + entities.length === 0 + ? [] + : [{ "system-entities": entities }], + } + : { "system-entities": entities }, + ), + exitCode: 0, + }; + } + throw new Error(`Unexpected command ${String(args[0])}`); + }, + }; + const reader = new NativeDmgArtifactReader("/fixture/image.dmg", {}, host); + const paths: string[] = []; + try { + for await (const entry of reader.entries(undefined, { + onCoverage: (value) => coverage.push(value), + })) + paths.push(entry.path); + expect(paths).toContain("image.dmg/Volume/readable.txt"); + expect(coverage).toEqual(unknownCoverage ? [producerCoverage] : []); + } finally { + try { + await reader.close(); + } finally { + traversal.mockRestore(); + } + } + }, +); it("retains malformed successful hdiutil inventory output as a typed producer parse failure before attach", async () => { const calls: string[] = [];