Return the validated descriptor and its original metadata from regular-file admission. Propagate that contract through owned reads, hashes, snapshots, artifact inventory and provider file workflows instead of replacing initial facts with another stat. Keep arbitrary output-handle operations separate. Preserve final descriptor/path identity proofs, borrowed-source verification, BigInt precision, cancellation provenance and failed-close ownership. Remove the obsolete source and owned-ZIP baseline stages and update their consumers and regressions. Tie read completion faults to actual reads instead of the retired duplicate stat; instrument real handles without fake handle wrappers. Validation: fresh runtime/catalog build; focused source, lifecycle, filesystem, CLI/MCP and authored provider-fixture tests; full static checks; docs checks; test discovery; duplication check. Platform-specific cases remain skipped on macOS. No installed provider or device behavior was verified.
168 lines
5.7 KiB
TypeScript
168 lines
5.7 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import type { BigIntStats } from "node:fs";
|
|
import { lstat, type FileHandle } from "node:fs/promises";
|
|
import { isAbsolute, relative, sep } from "node:path";
|
|
|
|
import {
|
|
NonRegularFileReadError,
|
|
isSelectedRegularFileError,
|
|
sameRegularFileState,
|
|
} from "../../filesystem/RegularFile.js";
|
|
import {
|
|
ArtifactReaderFailure,
|
|
sameArtifactSourceIdentity,
|
|
type ArtifactEntry,
|
|
} from "../ArtifactReader.js";
|
|
import {
|
|
assertArtifactReadActive,
|
|
mapArtifactFileFailure,
|
|
withArtifactRegularFile,
|
|
} from "../ArtifactFileOperation.js";
|
|
import type { ArtifactResourceScope } from "../ArtifactResourceScope.js";
|
|
import { readArtifactFileChunks } from "../readArtifactFileChunks.js";
|
|
import { hasMachoMagic, readMachoImage } from "./MachoLoadCommandReader.js";
|
|
|
|
/** One owner for dyld descriptor admission, physical identity and finalization. */
|
|
export class DylibFileAccess {
|
|
constructor(
|
|
private readonly resources: ArtifactResourceScope,
|
|
private readonly root: string,
|
|
private readonly signal?: AbortSignal,
|
|
) {}
|
|
|
|
startsWithMachoMagic(entry: ArtifactEntry): Promise<boolean> {
|
|
return this.#read(entry.adapterKey, async (file, initial) => {
|
|
if (!sameArtifactSourceIdentity(entry.sourceIdentity, initial))
|
|
throw changed(entry.adapterKey);
|
|
const bytes = new Uint8Array(4);
|
|
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
|
|
return hasMachoMagic(bytes.subarray(0, bytesRead));
|
|
});
|
|
}
|
|
|
|
image(path: string) {
|
|
return this.#read(path, async (file, identity) => {
|
|
const size = Number(identity.size);
|
|
if (!Number.isSafeInteger(size))
|
|
throw new ArtifactReaderFailure(
|
|
"limit",
|
|
`Mach-O file extent is not safely addressable: ${path}`,
|
|
);
|
|
const facts = await readMachoImage(async (offset, length) => {
|
|
assertArtifactReadActive(this.signal);
|
|
const bytes = new Uint8Array(
|
|
Math.max(0, Math.min(length, size - offset)),
|
|
);
|
|
const { bytesRead } = await file.read(bytes, 0, bytes.length, offset);
|
|
assertArtifactReadActive(this.signal);
|
|
return bytes.subarray(0, bytesRead);
|
|
}, size);
|
|
return { facts, identity };
|
|
});
|
|
}
|
|
|
|
sha256(path: string, expected: BigIntStats | undefined): Promise<string> {
|
|
return this.#read(path, async (file, initial) => {
|
|
if (expected !== undefined && !sameRegularFileState(expected, initial))
|
|
throw changed(path);
|
|
const hash = createHash("sha256");
|
|
// Admit the extent plus one byte to detect growth without following an
|
|
// actively appended file indefinitely. The descriptor remains owned.
|
|
const source = readArtifactFileChunks(file, {
|
|
end: initial.size,
|
|
signal: this.signal,
|
|
});
|
|
for await (const chunk of source) hash.update(chunk);
|
|
const coverage = source.readCoverage();
|
|
if (coverage.kind !== "complete" || coverage.bytes !== initial.size)
|
|
throw changed(path);
|
|
return hash.digest("hex");
|
|
});
|
|
}
|
|
|
|
async #read<Value>(
|
|
path: string,
|
|
operation: (file: FileHandle, initial: BigIntStats) => Promise<Value>,
|
|
): Promise<Value> {
|
|
try {
|
|
return await withArtifactRegularFile(
|
|
path,
|
|
this.resources,
|
|
this.signal,
|
|
async ({ handle: file }) => {
|
|
const initial = await file.stat({ bigint: true });
|
|
assertArtifactReadActive(this.signal);
|
|
const result = await operation(file, initial);
|
|
// Parsed headers, magic and digest must refer to the same physical
|
|
// version still selected by the pathname when the read completes.
|
|
const opened = await file.stat({ bigint: true });
|
|
let named: BigIntStats;
|
|
try {
|
|
named = await lstat(path, { bigint: true });
|
|
} catch (cause) {
|
|
if (!isSelectedRegularFileError(cause)) throw cause;
|
|
throw changed(path, cause);
|
|
}
|
|
if (
|
|
!sameRegularFileState(initial, opened) ||
|
|
!sameRegularFileState(initial, named)
|
|
)
|
|
throw changed(path);
|
|
assertArtifactReadActive(this.signal);
|
|
return result;
|
|
},
|
|
);
|
|
} catch (cause: unknown) {
|
|
throw classifyDylibReadFailure(this.root, cause);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Normalize dyld admission and permission facts inside their physical outcome. */
|
|
export const classifyDylibReadFailure = (
|
|
root: string,
|
|
cause: unknown,
|
|
): unknown =>
|
|
mapArtifactFileFailure(cause, (primary) => {
|
|
if (primary instanceof NonRegularFileReadError)
|
|
return new ArtifactReaderFailure(
|
|
"format",
|
|
`Dyld input is not a regular file: ${primary.path}`,
|
|
{ cause: primary },
|
|
);
|
|
if (
|
|
primary instanceof Error &&
|
|
"code" in primary &&
|
|
(primary.code === "EACCES" || primary.code === "EPERM")
|
|
) {
|
|
const path =
|
|
"path" in primary && typeof primary.path === "string"
|
|
? primary.path
|
|
: undefined;
|
|
const display =
|
|
path === undefined
|
|
? "a file in the analyzed root"
|
|
: deniedDisplayPath(root, path);
|
|
return new ArtifactReaderFailure(
|
|
"unavailable",
|
|
`Permission denied (${primary.code}) reading ${display}`,
|
|
{ cause: primary },
|
|
);
|
|
}
|
|
return primary;
|
|
});
|
|
|
|
const deniedDisplayPath = (root: string, path: string): string => {
|
|
const inside = relative(root, path);
|
|
const outside =
|
|
isAbsolute(inside) || inside === ".." || inside.startsWith(`..${sep}`);
|
|
return inside === "" || outside ? path : inside;
|
|
};
|
|
|
|
const changed = (path: string, cause?: unknown): ArtifactReaderFailure =>
|
|
new ArtifactReaderFailure(
|
|
"integrity",
|
|
`${path} changed while its dyld load commands were traced`,
|
|
cause === undefined ? undefined : { cause },
|
|
);
|