Files
reverse-engineering-anything/src/artifacts/NodeSeaArtifactReader.ts
T
morluto cb4e5cfbff feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts

* feat(analysis): expose ASAR and Ghidra runtime admission controls

* feat(artifacts): expose reader admission and preserve limited recognition

* feat(javascript): expose recoverable semantic precision budgets

* feat(web): add recoverable source-map budgets and compact retained views

* feat(web): make historical capture decoding budgets recoverable

* fix(desktop): enforce selected view policies across capture and export

* feat(runtime): make Frida and LLDB retention budgets selectable

* feat(native): expose diagnostic budgets and preserve exhaustion causes

* feat(managed): expose recoverable snapshot and representation budgets

* feat(metadata): make Go and Rust inspection budgets recoverable

* feat(wasm): expose representation and producer admissions

* feat(evm): expose per-call worker resource budgets

* feat(android): make resource and string-pool omissions recoverable

* feat(producers): expose command and recorded-capture budgets

* docs(analysis): record recovery contracts and integrated validation

* fix(docs): link calibration oracle from published verification pages

* fix(docs): restore desktop task anchors and receipt links

* test(analysis): align fixtures with selectable budget contracts

* fix(analysis): remove source-location projector dependency cycle

* test(analysis): refresh budget recovery and verifier regressions

* test(analysis): preserve format and publication recovery expectations

* test(windows): align conformance with owned runtime admission

* fix(desktop): preserve selected alias kinds in exported state

* test(analysis): align provider fixtures with recoverable budgets

* test(desktop): validate emitted alias metadata structurally

* fix(ghidra): classify bounded Windows endpoint refusals

* test(cli): assert selectable artifact admission recovery

* fix(cli): preserve capture provenance in environment builds
2026-11-10 05:11:20 -05:00

253 lines
8.0 KiB
TypeScript

import type { NodeSeaAdmission } from "./nodeSea/NodeSeaAdmission.js";
import {
DEFAULT_NODE_SEA_READER_LIMITS,
type NodeSeaReaderLimits,
} from "../domain/artifactReaderLimits.js";
import { setImmediate } from "node:timers/promises";
import { artifactExpansionBudgetBytes } from "../domain/artifactExpansionBudget.js";
import type { NodeSeaMetadata } from "../domain/artifactFormatMetadata.js";
import {
ArtifactPathRegistry,
normalizeArtifactPath,
} from "./ArtifactPaths.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import { artifactReaderLimitFailure } from "./ArtifactReaderLimits.js";
import {
decodeNodeSeaBlob,
type DecodedNodeSeaBlob,
type NodeSeaByteRange,
} from "./nodeSea/NodeSeaBlob.js";
import {
locateNodeSea,
readNodeSeaBlob,
scanNodeSeaFuse,
} from "./nodeSea/NodeSeaLocation.js";
import type { StableRegularFileDescriptor } from "../filesystem/RegularFile.js";
import {
PositionalArtifactReader,
type PositionalArtifactEntries,
type PositionalArtifactEntry,
} from "./PositionalArtifactReader.js";
const SNAPSHOT_PATH = "__node_sea__/snapshot";
const CODE_CACHE_PATH = "__node_sea__/code-cache";
const MAIN_FALLBACK_PATH = "__node_sea__/main.cjs";
const ASSET_FALLBACK_PREFIX = "__node_sea__/assets/";
type NodeSeaRole = "main-script" | "snapshot" | "code-cache" | "asset";
/** Read a Node.js SEA blob out of a Mach-O, ELF, or PE carrier without running it. */
export class NodeSeaArtifactReader extends PositionalArtifactReader {
override readonly format = "node-sea" as const;
#header: NodeSeaMetadata | undefined;
#admission: NodeSeaAdmission | undefined;
constructor(
path: string,
admitted?: StableRegularFileDescriptor | NodeSeaAdmission,
private readonly limits: NodeSeaReaderLimits = DEFAULT_NODE_SEA_READER_LIMITS,
private readonly expandedBytes?: number,
) {
const admission =
admitted !== undefined && "kind" in admitted ? admitted : undefined;
super(
path,
"Node SEA",
admitted !== undefined && "kind" in admitted ? admitted.source : admitted,
);
this.#admission = admission;
}
formatMetadata(): NodeSeaMetadata | undefined {
if (this.#header === undefined) return undefined;
return { ...this.#header, exec_argv: [...this.#header.exec_argv] };
}
/** Also drop the admitted decode, which only a completed preparation releases. */
override async close(): Promise<void> {
await super.close();
this.#admission = undefined;
}
/** Reuse only the decode admitted under this accepted policy. */
async #readBlob(
source: StableRegularFileDescriptor,
signal?: AbortSignal,
): Promise<Pick<NodeSeaAdmission, "location" | "decoded">> {
const admittedLimits = this.#admission?.limits;
if (
this.#admission !== undefined &&
admittedLimits !== undefined &&
Object.entries(this.limits).every(
([field, value]) => Reflect.get(admittedLimits, field) === value,
)
)
return this.#admission;
const location = await locateNodeSea(
source.handle,
source.initial.size,
signal,
this.limits,
);
if (location === undefined) throw formatFailure("blob was not found");
const decoded = await decodeNodeSeaBlob(
await readNodeSeaBlob(source, location, signal, this.limits),
signal,
this.limits,
);
return { location, decoded };
}
protected override async prepareEntries(
signal?: AbortSignal,
): Promise<PositionalArtifactEntries> {
const { handle, initial } = await this.verifyCarrier(signal);
const { location, decoded } = await this.#readBlob(
{ handle, initial },
signal,
);
const fuse = await scanNodeSeaFuse(handle, initial.size, signal);
const entries: PositionalArtifactEntry[] = [];
let declaredBytes = 0n;
const expansionBudget = BigInt(
this.expandedBytes ?? artifactExpansionBudgetBytes(location.blobLength),
);
let index = 0;
for (const member of seaMembers(decoded)) {
if (index % 256 === 0) await setImmediate();
this.assertOpen(signal);
declaredBytes += BigInt(member.range.length);
if (declaredBytes > expansionBudget)
throw artifactReaderLimitFailure(
"artifact_limits.expanded_bytes",
Number(expansionBudget),
"bytes",
declaredBytes,
"memory",
);
const start = location.blobOffset + member.range.offset;
if (start > initial.size || member.range.length > initial.size - start)
throw formatFailure(`member exceeds the carrier: ${member.path}`);
const entry = {
path: member.path,
kind: "file" as const,
declaredSize: member.range.length,
compressedSize: null,
executable: false,
encrypted: false,
byteOffset: start,
declaredIntegrity: null,
unpacked: false,
limitations: [],
adapterKey: `node-sea:${String(index)}`,
formatMetadata: {
kind: "node-sea-entry" as const,
role: member.role,
source_path: member.sourcePath,
index,
},
sourcePath: member.sourcePath,
} satisfies PositionalArtifactEntry;
entries.push(entry);
index += 1;
}
this.#header = {
kind: "node-sea",
magic: decoded.magic,
flags: decoded.flags,
disable_experimental_sea_warning: decoded.disableExperimentalSeaWarning,
use_snapshot: decoded.useSnapshot,
use_code_cache: decoded.useCodeCache,
include_assets: decoded.includeAssets,
include_exec_argv: decoded.includeExecArgv,
exec_argv_extension: decoded.execArgvExtension,
exec_argv: [...decoded.execArgv],
code_path: decoded.codePath,
carrier: location.carrier,
blob_offset: location.blobOffset,
blob_length: location.blobLength,
fuse_state: fuse.fuseState,
fuse_sentinel: fuse.fuseSentinel,
fuse_contradiction: fuse.fuseContradiction,
};
await this.verifyCarrier(signal);
this.#admission = undefined;
return {
entries,
coverage:
fuse.fuseContradiction === null
? undefined
: { status: "complete", limitations: [fuse.fuseContradiction] },
};
}
}
function* seaMembers(decoded: DecodedNodeSeaBlob): Generator<{
readonly path: string;
readonly sourcePath: string;
readonly role: NodeSeaRole;
readonly range: NodeSeaByteRange;
}> {
const registry = new ArtifactPathRegistry();
const claimed = new Set<string>();
const reserve = (path: string): string => {
if (claimed.has(path))
throw new ArtifactReaderFailure(
"path",
`Node SEA entry path collides: ${path}`,
);
registry.add(path, "file");
claimed.add(path);
return path;
};
const mappedPath = (raw: string, fallback: string): string => {
try {
const normalized = normalizeArtifactPath(raw);
if (
normalized !== "__node_sea__" &&
!normalized.startsWith("__node_sea__/")
)
return reserve(normalized);
} catch (cause) {
if (!(cause instanceof ArtifactReaderFailure) || cause.reason !== "path")
throw cause;
}
return reserve(fallback);
};
if (decoded.mainScript !== null)
yield {
path: mappedPath(decoded.codePath, MAIN_FALLBACK_PATH),
sourcePath: decoded.codePath,
role: "main-script",
range: decoded.mainScript,
};
if (decoded.snapshot !== null)
yield {
path: reserve(SNAPSHOT_PATH),
sourcePath: SNAPSHOT_PATH,
role: "snapshot",
range: decoded.snapshot,
};
if (decoded.codeCache !== null)
yield {
path: reserve(CODE_CACHE_PATH),
sourcePath: CODE_CACHE_PATH,
role: "code-cache",
range: decoded.codeCache,
};
for (const [ordinal, asset] of decoded.assets.entries())
yield {
path: mappedPath(
asset.name,
`${ASSET_FALLBACK_PREFIX}${String(ordinal)}`,
),
sourcePath: asset.name,
role: "asset",
range: asset.range,
};
}
const formatFailure = (detail: string): ArtifactReaderFailure =>
new ArtifactReaderFailure("format", `Node SEA blob is malformed: ${detail}`);