mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
Transfer the inventoried ZIP descriptor after inventory reader cleanup, then borrow it for materialization instead of reopening the pathname. Release the exact owner before publishing output; preserve failed owners for cleanup retry and completed inventory facts through cancellation or later tagged failures. This uses the existing admitted-descriptor reader API without a third carrier hash pass. Remove the superseded materialization wrapper. Cover identical-member carrier replacement, directory/missing replacements, late cancellation at both handoff and final close, mixed cleanup outcomes and retry. Root-owner test seams preserve real output-handle cleanup. Compiled CLI/MCP ZIP, archive and report regressions also run; no benchmark is claimed.
335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import type {
|
|
ArtifactAdmissionLimit,
|
|
ArtifactLimitBoundary,
|
|
} from "../domain/artifactOperationError.js";
|
|
import type {
|
|
ArtifactIntegrityClaim,
|
|
ArtifactIntegrityDetails,
|
|
} from "../domain/artifactIntegrity.js";
|
|
import type { ArtifactFormatMetadata } from "../domain/artifactFormatMetadata.js";
|
|
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import type { AnalysisCleanupLease } from "./AnalysisCleanupLease.js";
|
|
import type { Readable } from "node:stream";
|
|
import type {
|
|
ArtifactCommand,
|
|
ArtifactInventoryCoverage,
|
|
ArtifactTraversalOmissionKind,
|
|
} from "../domain/artifactGraph.js";
|
|
import type { ZipPackageFormat } from "../domain/zipPackageFormat.js";
|
|
import type {
|
|
AnalysisCleanupObservation,
|
|
AnalysisPartialObservation,
|
|
} from "../domain/analysisErrorBase.js";
|
|
|
|
export interface ArtifactReaderFailureOptions extends ErrorOptions {
|
|
readonly cancellation?: { readonly reason: unknown } | undefined;
|
|
readonly cleanup?: AnalysisCleanupObservation | undefined;
|
|
/** Exact authority for all reported cleanup; undefined leaves ownership unknown. */
|
|
readonly cleanupLeases?: readonly AnalysisCleanupLease[] | undefined;
|
|
readonly partialObservation?: AnalysisPartialObservation | undefined;
|
|
/** The constrained resource bound for a limit failure. */
|
|
readonly boundary?: ArtifactLimitBoundary;
|
|
/** Named admission whose recovery must not be inferred from other limit text. */
|
|
readonly admission?: ArtifactAdmissionLimit;
|
|
}
|
|
|
|
/** Archive-neutral entry metadata. Reader adapters never choose output paths. */
|
|
export interface ArtifactEntry {
|
|
readonly path: string;
|
|
/** Exact reader-source path or opaque key before logical path mapping. */
|
|
readonly sourcePath?: string;
|
|
readonly kind: "file" | "directory" | "symlink" | "slice" | "metadata";
|
|
readonly declaredSize: number | null;
|
|
readonly compressedSize: number | null;
|
|
readonly executable: boolean;
|
|
readonly encrypted: boolean;
|
|
readonly byteOffset: number | null;
|
|
readonly declaredIntegrity: ArtifactIntegrityClaim | null;
|
|
readonly formatMetadata?: ArtifactFormatMetadata;
|
|
readonly unpacked: boolean;
|
|
readonly limitations: readonly string[];
|
|
readonly adapterKey: string;
|
|
/** Filesystem identity captured during traversal, when supplied by an adapter. */
|
|
readonly sourceIdentity?: {
|
|
readonly device: bigint;
|
|
readonly inode: bigint;
|
|
readonly size: bigint;
|
|
readonly mode: bigint;
|
|
readonly mtimeNs: bigint;
|
|
readonly ctimeNs: bigint;
|
|
};
|
|
}
|
|
|
|
/** Compare captured filesystem identity and mutable metadata at a read boundary. */
|
|
export const sameArtifactSourceIdentity = (
|
|
expected: ArtifactEntry["sourceIdentity"],
|
|
observed: {
|
|
readonly dev: bigint;
|
|
readonly ino: bigint;
|
|
readonly size: bigint;
|
|
readonly mode: bigint;
|
|
readonly mtimeNs: bigint;
|
|
readonly ctimeNs: bigint;
|
|
},
|
|
): boolean =>
|
|
expected !== undefined &&
|
|
observed.dev === expected.device &&
|
|
observed.ino === expected.inode &&
|
|
observed.size === expected.size &&
|
|
observed.mode === expected.mode &&
|
|
observed.mtimeNs === expected.mtimeNs &&
|
|
observed.ctimeNs === expected.ctimeNs;
|
|
|
|
/** Snapshot mutable entry metadata before the producer later validates open(). */
|
|
export const copyArtifactEntry = (entry: ArtifactEntry): ArtifactEntry => ({
|
|
...entry,
|
|
limitations: [...entry.limitations],
|
|
declaredIntegrity:
|
|
entry.declaredIntegrity === null ? null : { ...entry.declaredIntegrity },
|
|
...(entry.formatMetadata === undefined
|
|
? {}
|
|
: { formatMetadata: { ...entry.formatMetadata } }),
|
|
...(entry.sourceIdentity === undefined
|
|
? {}
|
|
: { sourceIdentity: { ...entry.sourceIdentity } }),
|
|
});
|
|
|
|
/** Compare every portable and adapter-owned fact supplied with an entry. */
|
|
export const sameArtifactEntry = (
|
|
entry: ArtifactEntry,
|
|
produced: ArtifactEntry,
|
|
): boolean =>
|
|
entry.path === produced.path &&
|
|
entry.sourcePath === produced.sourcePath &&
|
|
entry.kind === produced.kind &&
|
|
entry.declaredSize === produced.declaredSize &&
|
|
entry.compressedSize === produced.compressedSize &&
|
|
entry.executable === produced.executable &&
|
|
entry.encrypted === produced.encrypted &&
|
|
entry.byteOffset === produced.byteOffset &&
|
|
sameDeclaredIntegrity(entry.declaredIntegrity, produced.declaredIntegrity) &&
|
|
sameFormatMetadata(entry.formatMetadata, produced.formatMetadata) &&
|
|
entry.unpacked === produced.unpacked &&
|
|
entry.adapterKey === produced.adapterKey &&
|
|
entry.limitations.length === produced.limitations.length &&
|
|
entry.limitations.every(
|
|
(limitation, index) => limitation === produced.limitations[index],
|
|
) &&
|
|
entry.sourceIdentity?.device === produced.sourceIdentity?.device &&
|
|
entry.sourceIdentity?.inode === produced.sourceIdentity?.inode &&
|
|
entry.sourceIdentity?.size === produced.sourceIdentity?.size &&
|
|
entry.sourceIdentity?.mode === produced.sourceIdentity?.mode &&
|
|
entry.sourceIdentity?.mtimeNs === produced.sourceIdentity?.mtimeNs &&
|
|
entry.sourceIdentity?.ctimeNs === produced.sourceIdentity?.ctimeNs;
|
|
|
|
const sameDeclaredIntegrity = (
|
|
left: ArtifactEntry["declaredIntegrity"],
|
|
right: ArtifactEntry["declaredIntegrity"],
|
|
): boolean =>
|
|
left === right ||
|
|
(left !== null &&
|
|
left !== undefined &&
|
|
right !== null &&
|
|
right !== undefined &&
|
|
left.algorithm === right.algorithm &&
|
|
left.digest === right.digest);
|
|
|
|
/** Format metadata contracts contain only primitive source fields. */
|
|
const sameFormatMetadata = (
|
|
left: ArtifactFormatMetadata | undefined,
|
|
right: ArtifactFormatMetadata | undefined,
|
|
): boolean =>
|
|
left === right ||
|
|
(left !== undefined &&
|
|
right !== undefined &&
|
|
Object.keys(left).length === Object.keys(right).length &&
|
|
Object.entries(left).every(
|
|
([key, value]) =>
|
|
Object.hasOwn(right, key) && Reflect.get(right, key) === value,
|
|
));
|
|
|
|
/** One entry or subtree deliberately excluded from a traversal. */
|
|
export interface ArtifactTraversalOmission {
|
|
readonly path: string;
|
|
readonly kind: ArtifactTraversalOmissionKind;
|
|
}
|
|
|
|
/** Facts from an exhausted traversal, separate from entry content certainty. */
|
|
export interface ArtifactReaderTraversal {
|
|
readonly status: ArtifactInventoryCoverage["status"];
|
|
readonly limitations: readonly string[];
|
|
/** Number of directory entries examined, including names omitted before decoding. */
|
|
readonly examinedEntries?: number;
|
|
}
|
|
|
|
/** Per-iterator observation sink; adapters report intentional omissions without shared state. */
|
|
export interface ArtifactTraversalOptions {
|
|
readonly onOmission?: (omission: ArtifactTraversalOmission) => void;
|
|
readonly onCoverage?: (coverage: ArtifactReaderTraversal) => void;
|
|
}
|
|
|
|
/** Read-only adapter over one directory, archive, or virtual container. */
|
|
export interface ArtifactReader {
|
|
readonly format:
|
|
| "directory"
|
|
| ZipPackageFormat
|
|
| "asar"
|
|
| "godot-pck"
|
|
| "pyinstaller"
|
|
| "node-sea"
|
|
| "file";
|
|
entries(
|
|
signal?: AbortSignal,
|
|
options?: ArtifactTraversalOptions,
|
|
): AsyncIterable<ArtifactEntry>;
|
|
open(entry: ArtifactEntry, signal?: AbortSignal): Promise<Readable>;
|
|
provenance(): readonly ArtifactCommand[];
|
|
formatMetadata?(): ArtifactFormatMetadata | undefined;
|
|
/** Bind materialization to the exact inventoried carrier before creating outputs. */
|
|
prepareContainer?(
|
|
expectedSha256: string,
|
|
signal?: AbortSignal,
|
|
): Promise<void>;
|
|
close(): Promise<void>;
|
|
}
|
|
|
|
/** Typed adapter failure translated at provider boundary. */
|
|
export class ArtifactReaderFailure extends Error {
|
|
readonly cancellation: { readonly reason: unknown } | undefined;
|
|
readonly cleanup: AnalysisCleanupObservation | undefined;
|
|
readonly cleanupLeases: readonly AnalysisCleanupLease[] | undefined;
|
|
partialObservation: AnalysisPartialObservation | undefined;
|
|
/** The constrained bound for a `limit` reason, when the reader knows it. */
|
|
readonly boundary: ArtifactLimitBoundary | undefined;
|
|
/** Named admission for a limit whose public recovery is owned by the producer. */
|
|
readonly admission: ArtifactAdmissionLimit | undefined;
|
|
|
|
constructor(
|
|
readonly reason:
|
|
| "cancelled"
|
|
| "format"
|
|
| "integrity"
|
|
| "io"
|
|
| "limit"
|
|
| "path"
|
|
| "unavailable",
|
|
message: string,
|
|
options?: ArtifactReaderFailureOptions,
|
|
readonly details?: ArtifactIntegrityDetails,
|
|
) {
|
|
super(message, options);
|
|
this.name = "ArtifactReaderFailure";
|
|
this.cancellation =
|
|
reason === "cancelled"
|
|
? (options?.cancellation ?? { reason: options?.cause })
|
|
: undefined;
|
|
this.cleanup = options?.cleanup;
|
|
this.cleanupLeases =
|
|
options?.cleanupLeases ?? (this.cleanup === undefined ? [] : undefined);
|
|
this.partialObservation = options?.partialObservation;
|
|
this.boundary = options?.boundary;
|
|
this.admission = options?.admission;
|
|
}
|
|
|
|
/** Retain completed facts when a later artifact step fails. */
|
|
retainPartialObservation(observation: AnalysisPartialObservation): this {
|
|
this.partialObservation ??= observation;
|
|
return this;
|
|
}
|
|
|
|
/** Preserve the primary failure and observation. Supplied leases must cover all merged cleanup. */
|
|
static withCleanup(
|
|
cause: unknown,
|
|
cleanup: AnalysisCleanupObservation,
|
|
partialObservation?: AnalysisPartialObservation,
|
|
cleanupLeases?: readonly AnalysisCleanupLease[],
|
|
): ArtifactReaderFailure {
|
|
const primary =
|
|
cause instanceof ArtifactReaderFailure
|
|
? cause
|
|
: new ArtifactReaderFailure("io", errorMessage(cause), {
|
|
cause,
|
|
...(cause instanceof AnalysisError
|
|
? {
|
|
partialObservation: cause.partialObservation,
|
|
...(cause.cleanupIncomplete
|
|
? {
|
|
cleanup: ArtifactReaderFailure.cleanupObservation(
|
|
cause,
|
|
[],
|
|
),
|
|
}
|
|
: {}),
|
|
}
|
|
: {}),
|
|
});
|
|
const mergedObservation = partialObservation ?? primary.partialObservation;
|
|
return new ArtifactReaderFailure(
|
|
primary.reason,
|
|
primary.message,
|
|
{
|
|
cause: primary,
|
|
cancellation: primary.cancellation,
|
|
cleanup: combineCleanup(primary.cleanup, cleanup),
|
|
cleanupLeases:
|
|
cleanupLeases ??
|
|
(cleanup === primary.cleanup ? primary.cleanupLeases : undefined),
|
|
// The constrained bound survives cleanup wrapping, or a file-size
|
|
// refusal would be reported as a memory constraint once a close fails.
|
|
...(primary.boundary === undefined
|
|
? {}
|
|
: { boundary: primary.boundary }),
|
|
...(primary.admission === undefined
|
|
? {}
|
|
: { admission: primary.admission }),
|
|
...(mergedObservation === undefined
|
|
? {}
|
|
: { partialObservation: mergedObservation }),
|
|
},
|
|
primary.details,
|
|
);
|
|
}
|
|
|
|
/** Give otherwise opaque cleanup failures a concrete owned resource. */
|
|
static cleanupObservation(
|
|
cause: unknown,
|
|
resource: string | readonly string[],
|
|
): AnalysisCleanupObservation {
|
|
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
|
|
return cause.cleanup;
|
|
if (cause instanceof AnalysisError && cause.cleanupIncomplete)
|
|
return {
|
|
reason: cause.cleanup?.reason ?? cause.message,
|
|
resources: [...cause.cleanupResources],
|
|
};
|
|
return {
|
|
reason: errorMessage(cause),
|
|
resources: typeof resource === "string" ? [resource] : [...resource],
|
|
};
|
|
}
|
|
}
|
|
|
|
const combineCleanup = (
|
|
primary: AnalysisCleanupObservation | undefined,
|
|
cleanup: AnalysisCleanupObservation,
|
|
): AnalysisCleanupObservation =>
|
|
primary === undefined || primary === cleanup
|
|
? cleanup
|
|
: {
|
|
reason: `${primary.reason}; ${cleanup.reason}`,
|
|
resources: [...new Set([...primary.resources, ...cleanup.resources])],
|
|
};
|
|
|
|
const errorMessage = (cause: unknown): string =>
|
|
cause instanceof Error ? cause.message : String(cause);
|
|
|
|
/** Read explicit source authority; unrelated domain cleanup remains unresolved. */
|
|
export const artifactPrimaryCleanupLeases = (
|
|
cause: unknown,
|
|
): readonly AnalysisCleanupLease[] | undefined =>
|
|
cause instanceof ArtifactReaderFailure
|
|
? cause.cleanupLeases
|
|
: cause instanceof AnalysisError && cause.cleanupIncomplete
|
|
? undefined
|
|
: [];
|