import { assertAdmittedRootDigest } from "../inventory/classify.js"; import { assertArtifactFileSelectedSource } from "../ArtifactFileSource.js"; import { assertArtifactSelectedSource, resolveArtifactSourcePath, } from "../ArtifactSelectedSource.js"; import { hashStableRootArtifactHandle } from "../inventory/hashStableRootArtifact.js"; import { absoluteCallerPath } from "../../filesystem/callerPath.js"; import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js"; import { createArtifactReader } from "../createArtifactReader.js"; import { resolveAsarHeaderAdmission } from "../AsarHeader.js"; import { ArtifactEmbeddedFormatProbes, type ArtifactEmbeddedFormatProbeFacts, } from "../ArtifactEmbeddedFormatProbes.js"; import { withArtifactRegularFile } from "../ArtifactFileOperation.js"; import type { ArtifactCleanupAttempt, ArtifactResourceOwner, ArtifactResourceScope, } from "../ArtifactResourceScope.js"; import { lstat, realpath } from "node:fs/promises"; import { dirname } from "node:path"; import { isPathWithinRoot } from "../../domain/localPath.js"; import { SafeOutputTreeDestinationRefusal } from "../SafeOutputTreeAdmission.js"; import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; import { ArtifactPathRegistry, normalizeArtifactPath, } from "../ArtifactPaths.js"; import { ArtifactReaderFailure, type ArtifactEntry, type ArtifactReader, type ArtifactReaderTraversal, } from "../ArtifactReader.js"; import { ArtifactTraversalObservation } from "../ArtifactTraversalObservation.js"; import { abortIfNeeded } from "../ArtifactHash.js"; import { SafeOutputTree } from "../SafeOutputTree.js"; import { SafeOutputTreeCreationFailure } from "../SafeOutputTreeCreationFailure.js"; import { isZipPackageFormat } from "../../domain/zipPackageFormat.js"; import { artifactExtractionResultSchema, type ArtifactExtractionResult, type ArtifactGraphManifest, type ArtifactCommand, type ArtifactNode, type ArtifactOccurrence, } from "../../domain/artifactGraph.js"; import { AnalysisUnsupportedTargetError } from "../../domain/analysisErrorCore.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js"; import { scanArtifactInventoryForExtractionInScope } from "../inventory/ArtifactInventory.js"; import type { TransferredArtifactInventorySource } from "../inventory/scanCanonical.js"; import type { ArtifactIntegrityPolicyName } from "../../domain/artifactIntegrityPolicy.js"; /** Local extraction input with the output root chosen by the adapter. */ export interface ArtifactExtractionInput { readonly asarHeaderAdmission?: AsarHeaderAdmission; readonly inputPath: string; readonly expectedRootSha256?: string | undefined; /** Selected artifact spelling; packed I/O remains on inputPath. */ readonly selectedPath?: string | undefined; readonly inputFormat: BinaryTarget["format"]; readonly outputRoot: string; readonly environment: Readonly; readonly integrityPolicy: ArtifactIntegrityPolicyName; readonly resourceScope: ArtifactResourceScope; } /** Extract every regular inventory occurrence into an exclusively owned absent root. */ export const extractArtifact = async ( input: ArtifactExtractionInput, signal?: AbortSignal, ): Promise => input.resourceScope.run(() => extractArtifactInScope(input, signal)); const extractArtifactInScope = async ( input: ArtifactExtractionInput, signal?: AbortSignal, ): Promise => { // Cancellation wins over every later refusal, including unsupported formats. abortIfNeeded(signal); const sourcePath = await resolveArtifactSourcePath(input.inputPath); if (input.expectedRootSha256 !== undefined) await assertArtifactSelectedSource(sourcePath, input.selectedPath); const probeFacts = await requireExtractableFormat(sourcePath, input, signal); const { snapshot, transferredSource } = await scanArtifactInventoryForExtractionInScope( input.inputPath, { resourceScope: input.resourceScope, selectedPath: input.selectedPath, expectedRootSha256: input.expectedRootSha256, signal, environment: input.environment, asarHeaderAdmission: input.asarHeaderAdmission ?? resolveAsarHeaderAdmission(input.environment), integrity: input.integrityPolicy, }, probeFacts, ); const activeSource = transferredSource === undefined ? undefined : { ...transferredSource, releaseAttempted: false }; try { return await materializeArtifactInventoryInScope( input, sourcePath, snapshot, signal, activeSource, ); } catch (cause: unknown) { if (activeSource !== undefined && cause instanceof ArtifactReaderFailure) cause.retainPartialObservation({ kind: "artifact-inventory", inventory: snapshot, }); const attempt = await releaseTransferredArtifactSource( input.resourceScope, activeSource, ); if (attempt?.kind === "failed") throw input.resourceScope.cleanupFailure( { kind: "failed", cause }, [attempt], { kind: "artifact-inventory", inventory: snapshot }, ); throw cause; } }; interface ActiveArtifactInventorySource extends TransferredArtifactInventorySource { releaseAttempted: boolean; } const releaseTransferredArtifactSource = async ( resourceScope: ArtifactResourceScope, transferredSource: ActiveArtifactInventorySource | undefined, ): Promise => { if (transferredSource === undefined || transferredSource.releaseAttempted) return undefined; transferredSource.releaseAttempted = true; return resourceScope.release(transferredSource.owner); }; /** Materialize a scanned inventory, borrowing its admitted source when supplied. */ export const materializeArtifactInventoryInScope = async ( input: ArtifactExtractionInput, sourcePath: string, snapshot: ArtifactInventorySnapshot, signal?: AbortSignal, transferredSource?: ActiveArtifactInventorySource, ): Promise => { abortIfNeeded(signal); // A directory source must never acquire its own output as a new member on // the verification traversal. Resolve the selected parent before any write. if (transferredSource === undefined) { const canonicalSource = await realpath(sourcePath); if ((await lstat(canonicalSource)).isDirectory()) { const { canonicalOutput } = await SafeOutputTree.resolveDestination( input.outputRoot, ); const parent = dirname(canonicalOutput); if (isPathWithinRoot(canonicalSource, parent)) throw new SafeOutputTreeDestinationRefusal( `Extraction output parent ${parent} is inside source directory ${canonicalSource}. Choose a temporary directory outside the source.`, ); } } abortIfNeeded(signal); const containerMetadata = snapshot.occurrences.find( (row) => row.logical_path === ".", )?.format_metadata; if ( containerMetadata?.kind === "godot-pck" && containerMetadata.directory_status !== "available" ) throw new AnalysisUnsupportedTargetError( "extract_artifact", input.selectedPath ?? input.inputPath, `Godot PCK directory is ${containerMetadata.directory_status}; its complete contents cannot be extracted`, ); if ( containerMetadata?.kind === "unityfs" && containerMetadata.directory_status !== "available" ) throw new AnalysisUnsupportedTargetError( "extract_artifact", input.selectedPath ?? input.inputPath, `UnityFS directory is ${containerMetadata.directory_status}; its complete contents cannot be extracted`, ); const selectedOccurrences = snapshot.occurrences.filter( (occurrence) => (occurrence.entry_kind === "file" || occurrence.entry_kind === "slice") && occurrence.logical_path !== ".", ); const regularPaths = new Set( selectedOccurrences.map(({ logical_path: path }) => path), ); const directoryPaths = new Set( snapshot.occurrences .filter(({ entry_kind }) => entry_kind === "directory") .map(({ logical_path }) => logical_path), ); const activeOccurrences = selectedOccurrences.filter(({ logical_path }) => { // Nested members remain represented by their containing regular file; // only entries exposed by the active reader are materialized. let path = logical_path; for ( let slash = path.lastIndexOf("/"); slash >= 0; slash = path.lastIndexOf("/") ) { path = path.slice(0, slash); if (regularPaths.has(path) && !directoryPaths.has(path)) return false; } return true; }); const neededNodes = new Set( activeOccurrences.map(({ artifact_id: id }) => id), ); const nodes = new Map(); for (const node of snapshot.nodes) if (neededNodes.has(node.artifact_id)) nodes.set(node.artifact_id, node); const selected = activeOccurrences.map((occurrence) => { // REA never decrypts archive entries, so an encrypted entry makes the // complete extraction unsupported rather than the archive invalid. if (occurrence.encrypted) throw new AnalysisUnsupportedTargetError( "extract_artifact", input.selectedPath ?? input.inputPath, `Archive entry ${occurrence.logical_path} is encrypted; REA does not decrypt archive entries, so the archive cannot be extracted completely`, ); // Inventory retains unavailable source occurrences, but extraction cannot // materialize bytes it never observed. if ( occurrence.entry_kind === "file" && occurrence.artifact_id === null && occurrence.hash_status === "unavailable" ) throw new ArtifactReaderFailure( "unavailable", `Inventoried source bytes are unavailable for ${occurrence.logical_path}`, undefined, occurrence.unpacked === true ? { logicalPath: occurrence.logical_path, declaredSha256: null, calculatedSha256: null, unpacked: true, } : undefined, ); if ( (occurrence.entry_kind !== "file" && occurrence.entry_kind !== "slice") || occurrence.artifact_id === null || occurrence.logical_path === "." ) throw new ArtifactReaderFailure( "format", `Selected occurrence is not an extractable regular child file: ${occurrence.logical_path} (${occurrence.occurrence_id})`, ); const node = nodes.get(occurrence.artifact_id); if (node === undefined) throw new ArtifactReaderFailure( "integrity", `Selected occurrence has no inventory node: ${occurrence.occurrence_id}`, ); return { occurrence, node }; }); return materializeSelection({ input, sourcePath, inventory: snapshot, selected, signal, transferredSource, }); }; interface SelectedOccurrence { readonly occurrence: ArtifactOccurrence; readonly node: ArtifactNode; } interface ExtractedOccurrence { readonly artifact_id: string; readonly relative_path: string; readonly sha256: string; readonly bytes_written: number; readonly created: true; } const materializeSelection = async ({ input, sourcePath, inventory, selected, signal, transferredSource, }: { readonly input: ArtifactExtractionInput; readonly sourcePath: string; readonly inventory: ArtifactInventorySnapshot; readonly selected: readonly SelectedOccurrence[]; readonly signal: AbortSignal | undefined; readonly transferredSource: ActiveArtifactInventorySource | undefined; }): Promise => { const byPath = new Map(); for (const item of selected) { const path = item.occurrence.logical_path; if (byPath.has(path)) throw new ArtifactReaderFailure( "path", `Artifact extraction path collision: ${JSON.stringify(path)}. Multiple selected occurrences share this normalized logical path; they cannot be materialized at the same destination path, even when the archive is valid. Inventory retains each occurrence.`, ); byPath.set(path, item); } if (transferredSource !== undefined && "kind" in transferredSource.admission) await assertArtifactFileSelectedSource( sourcePath, transferredSource.admission.source, "Artifact extraction", ); const reader = createReader( sourcePath, input, inventory.manifest.root_format, transferredSource, ); const readerOwner: ArtifactResourceOwner = { kind: "reader" as const, reader, resource: `artifact reader for ${sourcePath}`, }; let localReaderOwner: ArtifactResourceOwner | undefined = readerOwner; let output: SafeOutputTree | undefined; let completed: ArtifactExtractionResult | undefined; const extracted: ExtractedOccurrence[] = []; try { await reader.prepareContainer?.(inventory.manifest.root_sha256, signal); try { output = await SafeOutputTree.create(input.outputRoot); } catch (cause: unknown) { if (cause instanceof SafeOutputTreeCreationFailure) output = cause.tree; throw cause; } const traversal = new ArtifactTraversalObservation(); traversal.onCoverage({ status: inventory.coverage.status, limitations: inventory.limitations, }); await writeSelectedEntries({ reader, output, byPath, extracted, traversal, signal, }); localReaderOwner = undefined; const closeAttempt = await input.resourceScope.release(readerOwner); if (closeAttempt.kind === "failed") throw input.resourceScope.cleanupFailure( { kind: "completed" }, [closeAttempt], { kind: "artifact-inventory", inventory }, ); const sourceClose = await releaseTransferredArtifactSource( input.resourceScope, transferredSource, ); if (sourceClose?.kind === "failed") throw input.resourceScope.cleanupFailure( { kind: "completed" }, [sourceClose], { kind: "artifact-inventory", inventory }, ); if (transferredSource !== undefined) abortIfNeeded(signal); extracted.sort((left, right) => compareUnicodeCodePoints(left.relative_path, right.relative_path), ); completed = createExtractionResult( input, inventory, selected, extracted, traversal.snapshot(), reader.provenance(), ); if (input.expectedRootSha256 !== undefined) await assertArtifactSelectedSource(sourcePath, input.selectedPath); await output.commit(signal); return completed; } catch (cause: unknown) { const cleanupFailures = await releaseExtractionOwners( input.resourceScope, localReaderOwner, output, input.outputRoot, ); const [firstCleanup, ...remainingCleanup] = cleanupFailures; if (firstCleanup !== undefined) { throw input.resourceScope.cleanupFailure( { kind: "failed", cause }, [firstCleanup, ...remainingCleanup], output?.published === true && completed !== undefined ? { kind: "artifact-extraction", extraction: completed } : undefined, ); } if (output?.published === true && completed !== undefined) return completed; throw cause; } }; const writeSelectedEntries = async ({ reader, output, byPath, extracted, traversal, signal, }: { readonly reader: ArtifactReader; readonly output: SafeOutputTree; readonly byPath: Map; readonly extracted: ExtractedOccurrence[]; readonly traversal: ArtifactTraversalObservation; readonly signal: AbortSignal | undefined; }): Promise => { const registry = new ArtifactPathRegistry(); for await (const entry of reader.entries(signal, traversal)) { const path = normalizeArtifactPath(entry.path); registry.add(path, entry.kind); // Logical declarations may share a query path with a real file. They do // not consume that file's selection or participate in materialization. if (entry.kind !== "file" && entry.kind !== "slice") continue; const selectedItem = byPath.get(path); if (selectedItem === undefined) { throw new ArtifactReaderFailure( "integrity", `Regular artifact entry is missing from inventory: ${path}`, ); } preflight(entry); const stream = await reader.open(entry, signal); const written = await output.write( path, stream, { sha256: selectedItem.node.sha256, bytes: selectedItem.node.size, }, signal, ); extracted.push({ artifact_id: selectedItem.node.artifact_id, relative_path: written.relativePath, sha256: written.sha256, bytes_written: written.bytesWritten, created: true, }); byPath.delete(path); } if (byPath.size > 0) throw new ArtifactReaderFailure( "integrity", `Inventoried regular artifact entries were not materialized: ${[...byPath.keys()].sort(compareUnicodeCodePoints).join(", ")}`, ); }; const releaseExtractionOwners = async ( resourceScope: ArtifactResourceScope, readerOwner: ArtifactResourceOwner | undefined, output: SafeOutputTree | undefined, outputRoot: string, ): Promise[]> => { const failures: Extract< ArtifactCleanupAttempt, { readonly kind: "failed" } >[] = []; if (readerOwner !== undefined) { const attempt = await resourceScope.release(readerOwner); if (attempt.kind === "failed") failures.push(attempt); } if (output !== undefined) { const owner: ArtifactResourceOwner = { kind: "output-tree", tree: output, resource: outputRoot, }; const attempt = await resourceScope.release(owner); if (attempt.kind === "failed") failures.push(attempt); } return failures; }; const createExtractionResult = ( input: ArtifactExtractionInput, inventory: ArtifactInventorySnapshot, selected: readonly SelectedOccurrence[], extracted: readonly ExtractedOccurrence[], traversal: ArtifactReaderTraversal, materializationProvenance: readonly ArtifactCommand[], ): ArtifactExtractionResult => { const selectedIds = new Set( selected.map(({ occurrence }) => occurrence.occurrence_id), ); const integrityContradictions = inventory.integrity_contradictions.filter( ({ occurrence_id: id }) => selectedIds.has(id), ); const extractionSemantic = { source_manifest_id: inventory.manifest.manifest_id, selected_occurrence_ids: selected .map(({ occurrence }) => occurrence.occurrence_id) .sort(compareUnicodeCodePoints), files_sha256: digestCanonicalValue(extracted, "Artifact"), output_root_alias: "$OUTPUT_ROOT" as const, }; return artifactExtractionResultSchema.parse({ manifest: inventory.manifest, extraction_manifest: { ...extractionSemantic, extraction_id: `aex_${digestCanonicalValue(extractionSemantic, "Artifact")}`, }, output_root: input.outputRoot, artifacts: extracted, containment_verified: true, cleanup: { attempted: false, verified: true, residual_paths: [] }, provenance: [...inventory.provenance, ...materializationProvenance], coverage: { ...inventory.coverage, status: traversal.status, }, integrity_contradictions: integrityContradictions, limitations: [ ...traversal.limitations, ...selected.flatMap(({ occurrence }) => occurrence.limitations.map( (limitation) => `${occurrence.logical_path}: ${limitation}`, ), ), traversal.status === "complete" ? "All regular files exposed by the active artifact reader were materialized." : "All inventoried regular files exposed by the active reader were materialized; source traversal coverage is unknown, so the output is not established as a complete source copy.", "Publication verifies the selected inventoried regular files; it does not establish complete content decoding.", ...(integrityContradictions.length === 0 ? [] : [ `${String(integrityContradictions.length)} extracted file(s) contradict declared integrity metadata; their bytes are observed-untrusted.`, ]), ], }); }; /** * Refuse a format without an extraction reader before inventory work starts. * The target kind, not the host, is unsupported by extraction. The error keeps * the caller's spelling of the path; I/O uses the canonical path. */ const requireExtractableFormat = async ( path: string, input: Pick< ArtifactExtractionInput, | "inputPath" | "selectedPath" | "inputFormat" | "resourceScope" | "expectedRootSha256" >, signal?: AbortSignal, ): Promise => { const format = input.inputFormat; const directory = (await lstat(path)).isDirectory(); // A thin Mach-O has no child entries of its own: inventory lists only its // root unless it carries an embedded pack, so it takes the same pack probe // as ELF and PE while only the universal container has a slice reader. if ( !directory && format !== "asar" && format !== "godot-pck" && format !== "unityfs" && format !== "uimage" && format !== "intel-hex" && format !== "srec" && format !== "esp-image" && format !== "mach-o-universal" && !isZipPackageFormat(format) ) { const detected = await withArtifactRegularFile( path, input.resourceScope, signal, async (source) => { const probes = new ArtifactEmbeddedFormatProbes(path, source, signal); const pack = (await probes.godotPck()) !== undefined || ((format === "elf" || format === "pe" || format === "mach-o") && ((await probes.nodeSea()) !== undefined || (await probes.pyInstaller()) !== undefined)); if (pack && input.expectedRootSha256 !== undefined) { const digest = await hashStableRootArtifactHandle( path, source.handle, source.initial, signal, ); assertAdmittedRootDigest( path, input.expectedRootSha256, digest.sha256, ); } return { pack, probeFacts: probes.completedFacts() }; }, ); if (!detected.pack) throw new AnalysisUnsupportedTargetError( "extract_artifact", input.selectedPath ?? input.inputPath, `Artifact format has no extraction reader: ${format}`, ); return detected.probeFacts; } return undefined; }; /** Reader selection belongs to inventory classification, not the adapter hint. */ const createReader = ( path: string, input: Pick< ArtifactExtractionInput, "inputPath" | "selectedPath" | "environment" | "asarHeaderAdmission" >, containerFormat: ArtifactGraphManifest["root_format"], transferredSource?: ActiveArtifactInventorySource, ): ArtifactReader => { // DMG inventory may mount read-only, but extraction has no DMG authority. const reader = containerFormat === "dmg" ? undefined : createArtifactReader(path, containerFormat, { environment: input.environment, // Inventory already records inaccessible descendants as omissions. // Keep that traversal policy here; the selected-path check below // still refuses any inventoried file that cannot be materialized. unreadableDirectories: "omit", selectedPath: absoluteCallerPath( input.selectedPath ?? input.inputPath, ), admission: transferredSource?.admission, asarHeaderAdmission: containerFormat === "asar" ? (input.asarHeaderAdmission ?? resolveAsarHeaderAdmission(input.environment)) : undefined, }); if (reader === undefined) throw new AnalysisUnsupportedTargetError( "extract_artifact", input.selectedPath ?? input.inputPath, `Artifact format has no extraction reader: ${containerFormat}`, ); return reader; }; const preflight = (entry: ArtifactEntry): void => { if ((entry.kind !== "file" && entry.kind !== "slice") || entry.encrypted) throw new ArtifactReaderFailure( "format", `Selected artifact entry cannot be read: ${entry.path}`, ); };