import { mkdtemp, realpath, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import type { Readable } from "node:stream"; import { parse } from "plist"; import { z } from "zod"; import type { ArtifactCommand } from "../domain/artifactGraph.js"; import { ArtifactReaderFailure, type ArtifactEntry, type ArtifactReader, } from "./ArtifactReader.js"; import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js"; import { execFileOutput } from "../process/ExecFileOutput.js"; const DETACH_TIMEOUT_MS = 120_000; const attachOutputSchema = z.object({ "system-entities": z.array( z.object({ "dev-entry": z.string().startsWith("/dev/"), "mount-point": z.string().optional(), }), ), }); const infoOutputSchema = z.object({ images: z.array( z.object({ "system-entities": z.array(z.object({ "dev-entry": z.string() })), }), ), }); /** Narrow host seam for tested, shell-free hdiutil lifecycle operations. */ export interface NativeDmgHost { run( arguments_: readonly string[], signal?: AbortSignal, options?: { readonly timeoutMs: number }, ): Promise<{ readonly stdout: string; readonly exitCode: number }>; } const systemHost: NativeDmgHost = { async run(arguments_, signal, options) { try { const { stdout } = await execFileOutput( "/usr/bin/hdiutil", [...arguments_], { ...(options === undefined ? {} : { timeout: options.timeoutMs }), ...(signal === undefined ? {} : { signal }), }, ); return { stdout, exitCode: 0 }; } catch (cause: unknown) { if (cause instanceof Error && cause.name === "AbortError") throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled"); throw new ArtifactReaderFailure("format", "hdiutil rejected the DMG", { cause, }); } }, }; /** Read-only macOS DMG adapter that owns attachment and reverse-order detach. */ export class NativeDmgArtifactReader implements ArtifactReader { readonly format = "file" as const; readonly #provenance: ArtifactCommand[] = []; #directory: DirectoryArtifactReader | undefined; #devices: string[] = []; #mountRoot: string | undefined; private constructor( private readonly path: string, private readonly host: NativeDmgHost, ) {} /** Verify and attach one image beneath an exclusively owned temporary root. */ static async create( path: string, signal?: AbortSignal, host: NativeDmgHost = systemHost, ): Promise { if (process.platform !== "darwin" && host === systemHost) throw new ArtifactReaderFailure( "unavailable", "Native DMG traversal is available only on macOS", ); const reader = new NativeDmgArtifactReader(path, host); await reader.attach(signal); return reader; } async *entries(signal?: AbortSignal): AsyncIterable { if (this.#directory === undefined) throw new ArtifactReaderFailure("unavailable", "DMG is not attached"); const prefix = basename(this.path); for await (const entry of this.#directory.entries(signal)) yield { ...entry, path: `${prefix}/${entry.path}` }; } open(entry: ArtifactEntry, signal?: AbortSignal): Promise { if (this.#directory === undefined) return Promise.reject( new ArtifactReaderFailure("unavailable", "DMG is not attached"), ); return this.#directory.open(entry, signal); } provenance(): readonly ArtifactCommand[] { return structuredClone(this.#provenance); } async close(): Promise { let detachFailure: unknown; for (const device of [...this.#devices].reverse()) { try { await runChecked(this.host, ["detach", device], undefined, { timeoutMs: DETACH_TIMEOUT_MS, }); this.#provenance.push(command(["detach", device], ["mount"])); } catch (cause: unknown) { // Detaching a synthesized APFS container also ejects the image that // backs it, so a later whole disk of the same image may already be // gone. Only a device that is still attached is a cleanup failure. if (await this.#isAttached(device)) detachFailure ??= cause; } } this.#devices = []; if (this.#mountRoot !== undefined) await rm(this.#mountRoot, { recursive: true, force: true }).catch( (cause: unknown) => { detachFailure ??= cause; }, ); if (detachFailure !== undefined) throw new ArtifactReaderFailure( "unavailable", "DMG detach or mount-root cleanup failed", { cause: detachFailure }, ); } /** * Report whether hdiutil still lists a device; unknown state counts as * attached. Like detach, this cleanup query runs after the inventory * snapshot has captured provenance, so it is not recorded there. */ async #isAttached(device: string): Promise { try { const info = await runChecked(this.host, ["info", "-plist"], undefined, { timeoutMs: DETACH_TIMEOUT_MS, }); const parsed = infoOutputSchema.parse(parse(info.stdout)); return parsed.images.some((image) => image["system-entities"].some( (entity) => entity["dev-entry"] === device, ), ); } catch { return true; } } async attach(signal?: AbortSignal): Promise { await runChecked(this.host, ["verify", this.path], signal); this.#provenance.push(command(["verify", this.path], ["read"])); this.#mountRoot = await realpath(await mkdtemp(join(tmpdir(), "rea-dmg-"))); try { const attached = await runChecked( this.host, [ "attach", "-readonly", "-nobrowse", "-plist", "-mountroot", this.#mountRoot, this.path, ], signal, ); const parsed = attachOutputSchema.parse(parse(attached.stdout)); const devices = [ ...new Set( parsed["system-entities"].map((entity) => entity["dev-entry"]), ), ]; // Detaching an observed whole disk also detaches its partitions. Do not // subsequently detach the now-unavailable child devices. this.#devices = devices.filter( (device) => !devices.some( (parent) => /^\/dev\/disk\d+$/u.test(parent) && device.startsWith(`${parent}s`) && /^\d+$/u.test(device.slice(parent.length + 1)), ), ); if (this.#devices.length === 0) throw new ArtifactReaderFailure( "format", "hdiutil returned no attached devices", ); for (const entity of parsed["system-entities"]) if ( entity["mount-point"] !== undefined && !entity["mount-point"].startsWith(`${this.#mountRoot}/`) ) throw new ArtifactReaderFailure( "path", "hdiutil mounted outside the owned root", ); this.#provenance.push( command( [ "attach", "-readonly", "-nobrowse", "-plist", "-mountroot", this.#mountRoot, this.path, ], ["read", "mount"], ), ); this.#directory = new DirectoryArtifactReader(this.#mountRoot); } catch (cause: unknown) { let cleanupFailure: unknown; try { await this.close(); } catch (cleanupCause: unknown) { cleanupFailure = cleanupCause; } if (cleanupFailure !== undefined) throw new ArtifactReaderFailure( "unavailable", "DMG attach failed and cleanup could not detach every device", { cause: new AggregateError([cause, cleanupFailure]) }, ); throw cause; } } } const runChecked = async ( host: NativeDmgHost, arguments_: readonly string[], signal?: AbortSignal, options?: { readonly timeoutMs: number }, ): Promise<{ readonly stdout: string; readonly exitCode: 0 }> => { const result = await host.run(arguments_, signal, options); if (result.exitCode !== 0) throw new ArtifactReaderFailure( "unavailable", `hdiutil ${arguments_[0] ?? "operation"} failed with a non-zero exit code`, ); return { stdout: result.stdout, exitCode: 0 }; }; const command = ( arguments_: readonly string[], effects: ArtifactCommand["effects"], ): ArtifactCommand => ({ tool: "/usr/bin/hdiutil", arguments: [...arguments_], tool_version: null, executable_sha256: null, exit_code: 0, effects, });