Files
reverse-engineering-anything/src/pcap/PcapSnapshot.ts
T
morluto a891bb80ae refactor(filesystem): preserve admitted file facts
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.
2026-10-25 04:09:11 +08:00

310 lines
11 KiB
TypeScript

import { regularFileCancellation } from "../filesystem/OwnedRegularFile.js";
import { createHash } from "node:crypto";
import { chmod, open, stat, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import {
assertRegularFileActive,
openRegularFile,
RegularFileAdmissionFailure,
sameRegularFileState,
} from "../filesystem/RegularFile.js";
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
import {
RuntimeControlUnavailableError,
runtimeControlCapabilityError,
} from "../process/RuntimeControlUnavailableError.js";
import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js";
const OPERATION = "inspect_pcap_capture";
const MAX_CAPTURE_BYTES = 2 * 1024 ** 3;
/** An owned capture copy and every descriptor needed to release it safely. */
export class PcapSnapshot {
private readonly handles = new Set<OwnedFileHandle>();
private readonly completionCleanup = new Map<
OwnedFileHandle,
AnalysisError
>();
readonly path: string;
readonly configurationPath: string;
private constructor(private readonly root: PrivateRuntimeRoot) {
this.path = join(root.path, "capture.pcap");
this.configurationPath = join(root.path, "configuration");
}
static async create(): Promise<PcapSnapshot> {
try {
return new PcapSnapshot(
await PrivateRuntimeRoot.create({ prefix: "rea-pcap-" }),
);
} catch (cause) {
if (cause instanceof RuntimeControlUnavailableError)
throw runtimeControlCapabilityError("pcap", OPERATION, cause);
throw privateWorkspaceFailure("pcap", OPERATION, cause, null);
}
}
private own(handle: OwnedFileHandle): OwnedFileHandle {
this.handles.add(handle);
return handle;
}
private async release(handle: OwnedFileHandle): Promise<void> {
await handle.close();
this.handles.delete(handle);
this.completionCleanup.get(handle)?.resolveCleanup();
this.completionCleanup.delete(handle);
}
private async releaseCompleted(
handle: OwnedFileHandle,
signal?: AbortSignal,
): Promise<void> {
try {
await this.release(handle);
} catch (cleanupCause) {
const cleanup = new ProviderCleanupError(
"pcap",
[this.path],
{ reason: "Owned snapshot descriptor close failed" },
{ operation: OPERATION, cause: cleanupCause },
);
this.completionCleanup.set(handle, cleanup);
if (signal?.aborted !== true) throw cleanup;
const cancelled = new AnalysisCancelledError(OPERATION, {
cause: signal.reason,
cleanup: {
reason: "Owned snapshot descriptor close could not be confirmed",
resources: [this.path],
},
});
cancelled
.retainSecondaryFailure(cleanup)
.bindCleanupDependencies([cleanup]);
throw cancelled;
}
}
async capture(
path: string,
signal?: AbortSignal,
): Promise<{ bytes: number; sha256: string }> {
const sourceRead = async <T>(read: () => Promise<T>): Promise<T> => {
try {
return await read();
} catch (cause) {
const cancellation = regularFileCancellation(cause);
if (cancellation !== undefined) throw cause;
if (cause instanceof AnalysisError) throw cause;
if (
cause instanceof Error &&
"code" in cause &&
(cause.code === "EACCES" || cause.code === "EPERM")
)
throw new AnalysisAccessDeniedError(OPERATION, path, cause.code, {
cause,
});
throw new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Cannot read selected capture ${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
},
]);
}
};
try {
assertRegularFileActive(signal);
await mkdir(this.configurationPath, { mode: 0o700 });
const source = await sourceRead(async () => {
try {
const { handle } = await openRegularFile(path, {
symlinks: "follow",
signal,
});
return this.own(new OwnedFileHandle(handle));
} catch (cause) {
if (cause instanceof RegularFileAdmissionFailure) {
this.own(cause.owner);
throw cause.cause;
}
throw cause;
}
});
const before = await sourceRead(() =>
source.handle.stat({ bigint: true }),
);
if (before.size > BigInt(MAX_CAPTURE_BYTES))
throw new AnalysisResourceConstraintError(
OPERATION,
"file-size",
"Capture exceeds the owned-snapshot disk budget",
{ max_capture_bytes: MAX_CAPTURE_BYTES },
fixedAllowanceOptions(
"the owned capture snapshot allowance",
"input",
),
);
const destination = this.own(
new OwnedFileHandle(await open(this.path, "wx", 0o600)),
);
const buffer = Buffer.alloc(64 * 1024);
const hash = createHash("sha256");
let copied = 0;
while (copied <= Number(before.size)) {
assertRegularFileActive(signal);
const { bytesRead } = await sourceRead(() =>
source.handle.read(
buffer,
0,
Math.min(buffer.length, Number(before.size) - copied + 1),
copied,
),
);
assertRegularFileActive(signal);
if (bytesRead === 0) break;
copied += bytesRead;
if (BigInt(copied) > before.size)
throw new AnalysisArtifactChangedError(
OPERATION,
path,
"Capture grew during acquisition",
);
const chunk = buffer.subarray(0, bytesRead);
await destination.handle.writeFile(chunk);
hash.update(chunk);
}
const after = await sourceRead(() =>
source.handle.stat({ bigint: true }),
);
const selected = await stat(path, { bigint: true }).catch(
(cause: unknown) => {
if (
cause instanceof Error &&
"code" in cause &&
(cause.code === "EACCES" || cause.code === "EPERM")
)
throw new AnalysisAccessDeniedError(OPERATION, path, cause.code, {
cause,
});
throw new AnalysisArtifactChangedError(
OPERATION,
path,
"Selected path became unavailable during acquisition",
{ cause },
);
},
);
if (
BigInt(copied) !== before.size ||
!sameRegularFileState(before, after) ||
!sameRegularFileState(before, selected)
)
throw new AnalysisArtifactChangedError(
OPERATION,
path,
"Capture identity or contents changed during acquisition",
);
await this.releaseCompleted(source, signal);
await this.releaseCompleted(destination, signal);
await chmod(this.path, 0o400);
assertRegularFileActive(signal);
return { bytes: copied, sha256: hash.digest("hex") };
} catch (cause) {
if (cause instanceof RegularFileAdmissionFailure) this.own(cause.owner);
const primary =
cause instanceof RegularFileAdmissionFailure ? cause.cause : cause;
const cancellation = regularFileCancellation(primary);
if (cancellation !== undefined)
throw new AnalysisCancelledError(OPERATION, {
cause: cancellation.reason,
});
if (cause instanceof AnalysisError) throw cause;
throw privateWorkspaceFailure("pcap", OPERATION, primary, this.root.path);
}
}
/** Recheck byte identity after both tools; a read-only mode alone is not proof. */
async verify(
expected: { bytes: number; sha256: string },
selectedPath: string,
signal?: AbortSignal,
): Promise<void> {
try {
const { handle } = await openRegularFile(this.path, {
symlinks: "reject",
signal,
});
const file = this.own(new OwnedFileHandle(handle));
const before = await file.handle.stat({ bigint: true });
const buffer = Buffer.alloc(64 * 1024);
const hash = createHash("sha256");
let bytes = 0;
while (bytes <= expected.bytes) {
assertRegularFileActive(signal);
const read = await file.handle.read(
buffer,
0,
Math.min(buffer.length, expected.bytes - bytes + 1),
bytes,
);
if (read.bytesRead === 0) break;
bytes += read.bytesRead;
hash.update(buffer.subarray(0, read.bytesRead));
}
const after = await file.handle.stat({ bigint: true });
const selected = await stat(this.path, { bigint: true });
if (
bytes !== expected.bytes ||
hash.digest("hex") !== expected.sha256 ||
!sameRegularFileState(before, after) ||
!sameRegularFileState(before, selected)
)
throw new AnalysisArtifactChangedError(
OPERATION,
selectedPath,
"Owned packet snapshot changed during tool analysis.",
);
await this.releaseCompleted(file, signal);
assertRegularFileActive(signal);
} catch (cause) {
if (cause instanceof RegularFileAdmissionFailure) this.own(cause.owner);
const primary =
cause instanceof RegularFileAdmissionFailure ? cause.cause : cause;
const cancellation = regularFileCancellation(primary);
if (cancellation !== undefined)
throw new AnalysisCancelledError(OPERATION, {
cause: cancellation.reason,
});
if (cause instanceof AnalysisError) throw cause;
throw new AnalysisArtifactChangedError(
OPERATION,
selectedPath,
"Cannot establish the owned packet snapshot's post-analysis identity.",
{ cause },
);
}
}
async close(): Promise<void> {
const results = await Promise.allSettled(
[...this.handles].map((handle) => this.release(handle)),
);
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason as unknown] : [],
);
if (failures.length > 0)
throw new AggregateError(failures, "Capture descriptor cleanup failed");
await this.root.close();
}
}