mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:22:42 +00:00
refactor: remove redundant seams and preserve cleanup ownership (#10375)
* refactor: remove redundant seams and preserve cleanup ownership * fix: validate PE resource extents before budget checks
This commit is contained in:
1 parent
4b26f38d6f
commit
cdc4541ef7
369 files changed
+3847
-3662
No files matched your search
@@ -5,7 +5,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
/**
|
||||
* Reduce filesystem resolution during CLI and MCP startup. Keep dependencies
|
||||
* external, dynamic imports lazy, and original compiled worker entrypoints.
|
||||
* external and original compiled worker entrypoints. Dynamic imports can split
|
||||
* into lazy chunks unless the same module is also statically reachable.
|
||||
*/
|
||||
export const bundleRuntime = async (root) => {
|
||||
const compiledRoot = resolve(root, "dist");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { deepStrictEqual } from "node:assert";
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
@@ -13,7 +12,6 @@ import { dirname, isAbsolute, join } from "node:path";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { PRODUCT_IDENTITY } from "../dist/identity.js";
|
||||
import { pathExists } from "./lib/verify-package-core.mjs";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { skillReferenceIssues } from "./lib/docs-facts.mjs";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -13,7 +12,6 @@ import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { readGhidraLanguageCatalog } from "../dist/ghidra/GhidraLanguageCatalog.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
|
||||
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
|
||||
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
|
||||
import { buildGameCubeDolFixture } from "../tests/conformance/ghidra/dol-fixture.mjs";
|
||||
@@ -96,8 +94,6 @@ async function languageInstalled(languageId) {
|
||||
}
|
||||
}
|
||||
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
/** One MCP session plus CLI check. `languageOverride` is set only for the Gekko pass. */
|
||||
async function runPass({ fixture, languageOverride, label, layout }) {
|
||||
const env = { ...baseEnv };
|
||||
|
||||
@@ -185,7 +185,7 @@ const stringBindings = (sourceFile, symbols, unresolved) => {
|
||||
)
|
||||
return undefined;
|
||||
if ((declaration.parent.flags & ts.NodeFlags.Const) === 0) {
|
||||
unknown(identifier, "mutable setting-key or value alias");
|
||||
unknown(declaration.name, "mutable setting-key or value alias");
|
||||
return undefined;
|
||||
}
|
||||
if (declaration.pos >= identifier.pos) {
|
||||
@@ -919,7 +919,7 @@ if (direct) {
|
||||
const entries = parseNumericLimitRegistry(registry);
|
||||
const listed = new Map();
|
||||
for (const entry of entries) {
|
||||
const { name, policy, read, reason } = entry;
|
||||
const { name, read, reason } = entry;
|
||||
if (listed.has(name)) failures.push(`${name} is listed twice`);
|
||||
listed.set(name, entry);
|
||||
if (reason.trim().length < 40)
|
||||
|
||||
@@ -20,6 +20,7 @@ it.each([
|
||||
source:
|
||||
'process.stdout.write("partial-雪"); process.stderr.write("waiting\\n"); setInterval(() => {}, 1000)',
|
||||
code: "provider_timeout",
|
||||
timeoutMs: 500,
|
||||
stdout: "partial-雪",
|
||||
stderr: "waiting\n",
|
||||
},
|
||||
@@ -28,6 +29,7 @@ it.each([
|
||||
source:
|
||||
'process.stdout.write("partial-雪"); process.stderr.write("adb: error: device offline\\n"); process.exitCode = 1',
|
||||
code: "unsupported_target",
|
||||
expectedPath: "caller-device",
|
||||
stdout: "partial-雪",
|
||||
stderr: "adb: error: device offline\n",
|
||||
},
|
||||
@@ -36,6 +38,7 @@ it.each([
|
||||
source:
|
||||
'process.stdout.write("partial-雪"); process.stderr.write("adb: error: cannot stat \'/host/token file\': Permission denied\\n"); process.exitCode = 1',
|
||||
code: "unsupported_target",
|
||||
expectedPath: "/host/token file",
|
||||
stdout: "partial-雪",
|
||||
stderr: "adb: error: cannot stat '/host/token file': Permission denied\n",
|
||||
},
|
||||
@@ -54,7 +57,7 @@ it.each([
|
||||
},
|
||||
["-e", testCase.source],
|
||||
{
|
||||
timeoutMs: testCase.name === "timeout" ? 500 : 5_000,
|
||||
timeoutMs: testCase.timeoutMs ?? 5_000,
|
||||
maxOutputBytes: testCase.maxOutputBytes ?? 1024,
|
||||
},
|
||||
undefined,
|
||||
@@ -75,12 +78,34 @@ it.each([
|
||||
},
|
||||
});
|
||||
if (testCase.code === "unsupported_target")
|
||||
expect(projected.details?.path).toBe(
|
||||
testCase.name === "local path refusal"
|
||||
? "/host/token file"
|
||||
: "caller-device",
|
||||
);
|
||||
expect(projected.details?.path).toBe(testCase.expectedPath);
|
||||
if (testCase.code === "provider_timeout") {
|
||||
expect(projected.details?.timeout_ms).toBe(testCase.timeoutMs);
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
}
|
||||
expect(JSON.stringify(projected)).not.toContain(
|
||||
"ambient-value-must-not-be-persisted",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"projects a truncated adb capture as retryable only for volatile output (%s)",
|
||||
async (volatileOutput) => {
|
||||
const result = await runAdbCapture(
|
||||
volatileOutput ? "read_adb_logcat" : "install_adb_package",
|
||||
{ device: "device" },
|
||||
{ binary: process.execPath, pathSource: "environment", environment: {} },
|
||||
["-e", 'process.stdout.write("X".repeat(4096))'],
|
||||
{ timeoutMs: 5_000, maxOutputBytes: 1024, volatileOutput },
|
||||
undefined,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected a capture allowance refusal");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.retryable).toBe(volatileOutput);
|
||||
expect(projected).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: { reported_limits: { max_output_bytes: 1024 } },
|
||||
});
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
ExecFileOutputCancelledError,
|
||||
execFileBytes,
|
||||
@@ -212,65 +211,6 @@ it("does not infer cancellation from a foreign ABORT_ERR code", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"projects a truncated adb capture as retryable only for volatile output (%s)",
|
||||
async (volatileOutput) => {
|
||||
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
||||
Object.assign(new Error("too much"), {
|
||||
code: 1,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
outputTruncated: true,
|
||||
}),
|
||||
);
|
||||
const result = await runAdbCapture(
|
||||
volatileOutput ? "read_adb_logcat" : "install_adb_package",
|
||||
{ device: "device" },
|
||||
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
||||
volatileOutput ? ["logcat", "-d"] : ["install", "app.apk"],
|
||||
{ timeoutMs: 100, maxOutputBytes: 1024, volatileOutput },
|
||||
undefined,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected a capture allowance refusal");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.retryable).toBe(volatileOutput);
|
||||
expect(projected).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: { reported_limits: { max_output_bytes: 1024 } },
|
||||
});
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("reports the effective fixed adb deadline", async () => {
|
||||
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
||||
Object.assign(new Error("deadline"), {
|
||||
code: "ETIMEDOUT",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: true,
|
||||
}),
|
||||
);
|
||||
const result = await runAdbCapture(
|
||||
"pull_adb_file",
|
||||
{ device: "device" },
|
||||
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
||||
["pull", "/remote"],
|
||||
{ timeoutMs: 600_000, maxOutputBytes: 1024 },
|
||||
undefined,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected an adb deadline");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
details: { timeout_ms: 600_000 },
|
||||
});
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps a physical failure even when the later abort reason is that same error", async () => {
|
||||
const controller = new AbortController();
|
||||
const failure = new Error("physical command failure");
|
||||
|
||||
@@ -47,19 +47,32 @@ it("reports the fixed adb version-probe deadline", async () => {
|
||||
});
|
||||
|
||||
it("reports the fixed adb version-output cap", async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("too much"), {
|
||||
code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
outputTruncated: true,
|
||||
}),
|
||||
);
|
||||
const failure = await inspectAdbClient(selection).catch(
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
let selected: string = selection.binary;
|
||||
if (process.platform === "win32") {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("too much"), {
|
||||
code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
outputTruncated: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-adb-version-cap-"));
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }));
|
||||
selected = join(root, "adb");
|
||||
await writeFile(
|
||||
selected,
|
||||
`#!${process.execPath}\nprocess.stdout.write("x".repeat(100_000));\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
}
|
||||
const failure = await inspectAdbClient({
|
||||
...selection,
|
||||
binary: selected,
|
||||
}).catch((cause: unknown) => cause);
|
||||
const projected = projectAnalysisError(
|
||||
failure as Parameters<typeof projectAnalysisError>[0],
|
||||
);
|
||||
@@ -74,25 +87,28 @@ it("reports the fixed adb version-output cap", async () => {
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it("distinguishes execute denial from missing or unreadable version output", async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("denied"), {
|
||||
code: "EACCES",
|
||||
spawnFailed: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
}),
|
||||
);
|
||||
await expect(inspectAdbClient(selection)).rejects.toMatchObject({
|
||||
code: "binary_not_executable",
|
||||
diagnostics: {
|
||||
selected_binary: selection.binary,
|
||||
filesystem_error: "EACCES",
|
||||
},
|
||||
});
|
||||
});
|
||||
it.skipIf(process.platform !== "win32")(
|
||||
"classifies an EACCES execute refusal on Windows",
|
||||
async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("denied"), {
|
||||
code: "EACCES",
|
||||
spawnFailed: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
}),
|
||||
);
|
||||
await expect(inspectAdbClient(selection)).rejects.toMatchObject({
|
||||
code: "binary_not_executable",
|
||||
diagnostics: {
|
||||
selected_binary: selection.binary,
|
||||
filesystem_error: "EACCES",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("names an EPERM execute refusal separately from missing permission", async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
|
||||
@@ -313,10 +313,10 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
: AbortSignal.any([signal, this.#shutdown.signal]),
|
||||
});
|
||||
this.#inspections.add(pending);
|
||||
void pending.then(
|
||||
() => this.#inspections.delete(pending),
|
||||
() => this.#inspections.delete(pending),
|
||||
);
|
||||
const remove = (): void => {
|
||||
this.#inspections.delete(pending);
|
||||
};
|
||||
void pending.then(remove, remove);
|
||||
return pending;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as commandOwner from "../process/OwnedCommand.js";
|
||||
import { failedInspectionDescriptor } from "../../tests/fixtures/processInspectionCleanup.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import {
|
||||
AnalysisError,
|
||||
primaryAnalysisError,
|
||||
@@ -486,6 +487,7 @@ it("cancels a pending filesystem lookup and owns its eventual rejection", async
|
||||
const pendingStat = new Promise((_resolve, reject) => {
|
||||
rejectLookup = reject;
|
||||
});
|
||||
void pendingStat.catch(() => undefined);
|
||||
const observed = new Promise<void>((resolve) => {
|
||||
started = resolve;
|
||||
});
|
||||
@@ -502,14 +504,20 @@ it("cancels a pending filesystem lookup and owns its eventual rejection", async
|
||||
"win32",
|
||||
controller.signal,
|
||||
);
|
||||
await observed;
|
||||
controller.abort();
|
||||
await expect(pending).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
rejectLookup(new Error("filesystem completed after cancellation"));
|
||||
await Promise.resolve();
|
||||
expect(lookup.stat).toHaveBeenCalledTimes(1);
|
||||
try {
|
||||
await awaitAcknowledgement(observed, pending, "apktool filesystem lookup");
|
||||
controller.abort();
|
||||
await expect(pending).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
rejectLookup(new Error("filesystem completed after cancellation"));
|
||||
await Promise.resolve();
|
||||
expect(lookup.stat).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
controller.abort();
|
||||
rejectLookup(new Error("fixture cleanup"));
|
||||
await pending.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds the entire pending PATH lookup by the selection deadline", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import {
|
||||
getNavigationContext,
|
||||
@@ -500,7 +501,16 @@ it("waits for late facet cleanup after cancellation and retains its owner", asyn
|
||||
settled = true;
|
||||
return result;
|
||||
});
|
||||
await started.promise;
|
||||
onTestFinished(async () => {
|
||||
controller.abort();
|
||||
late.resolve(err(failure));
|
||||
try {
|
||||
await pending;
|
||||
} finally {
|
||||
await lease.release();
|
||||
}
|
||||
});
|
||||
await awaitAcknowledgement(started.promise, pending, "facet execution");
|
||||
controller.abort();
|
||||
await Promise.resolve();
|
||||
expect(settled).toBe(false);
|
||||
@@ -530,7 +540,12 @@ it("reports cancellation after a late successful facet that ignored its signal",
|
||||
{ address: "0x401000", document: "fixture" },
|
||||
controller.signal,
|
||||
);
|
||||
await started.promise;
|
||||
onTestFinished(async () => {
|
||||
controller.abort();
|
||||
late.resolve(ok(createAnalysisExecution("finished", provider)));
|
||||
await pending;
|
||||
});
|
||||
await awaitAcknowledgement(started.promise, pending, "facet execution");
|
||||
controller.abort();
|
||||
late.resolve(ok(createAnalysisExecution("finished", provider)));
|
||||
const result = await pending;
|
||||
@@ -586,7 +601,11 @@ it("preserves a provider's cancellation for the same signal", async () => {
|
||||
{ address: "0x401000", document: "fixture" },
|
||||
controller.signal,
|
||||
);
|
||||
await started.promise;
|
||||
onTestFinished(async () => {
|
||||
controller.abort(reason);
|
||||
await pending;
|
||||
});
|
||||
await awaitAcknowledgement(started.promise, pending, "provider execution");
|
||||
controller.abort(reason);
|
||||
const result = await pending;
|
||||
if (result.ok) throw new Error("expected cancellation");
|
||||
|
||||
@@ -67,10 +67,7 @@ import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import { profilelessNativeDispatchProfile } from "./binary/ProfilelessNativeWorkflowSnapshot.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
|
||||
import type {
|
||||
AnalysisExecution,
|
||||
ProviderIdentity,
|
||||
} from "./AnalysisProvider.js";
|
||||
import type { AnalysisExecution } from "./AnalysisProvider.js";
|
||||
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
|
||||
import { REA_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js";
|
||||
import { workflowAnalysisProfile } from "./WorkflowAnalysisProfile.js";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { MANAGED_SKILL_FILES } from "./SetupSkill.js";
|
||||
import { searchGuidanceOutputSchema } from "../contracts/guidanceContracts.js";
|
||||
import { RetainedRegularFileCleanupFailure } from "./RegularFileRead.js";
|
||||
|
||||
@@ -12,6 +12,7 @@ import { join, sep } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { ok } from "../domain/result.js";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
let resources: ArtifactResourceScope;
|
||||
@@ -612,17 +613,22 @@ describe("Registration cancellation and sibling outcomes", () => {
|
||||
entered();
|
||||
await blocked;
|
||||
});
|
||||
await admitted;
|
||||
const controller = new AbortController();
|
||||
const queued = register(path, launcher, controller.signal);
|
||||
controller.abort("queued cancellation");
|
||||
unblock();
|
||||
await first;
|
||||
expect(await queued).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError" },
|
||||
});
|
||||
expect(await readFile(path, "utf8")).toBe("{}");
|
||||
try {
|
||||
await awaitAcknowledgement(admitted, first, "registration transaction");
|
||||
const controller = new AbortController();
|
||||
const queued = register(path, launcher, controller.signal);
|
||||
controller.abort("queued cancellation");
|
||||
unblock();
|
||||
await first;
|
||||
expect(await queued).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError" },
|
||||
});
|
||||
expect(await readFile(path, "utf8")).toBe("{}");
|
||||
} finally {
|
||||
unblock();
|
||||
await first;
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps successful siblings after ordinary failures and marks work not started after cancellation", async () => {
|
||||
|
||||
@@ -23,7 +23,6 @@ import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
|
||||
import { DEFAULT_CATALOG_IDENTITY } from "../catalogIdentity.js";
|
||||
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
|
||||
import { currentClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
|
||||
/** Recording setup host for service-level planning and recovery tests. */
|
||||
export class FakeSetupHost implements SetupHost {
|
||||
|
||||
@@ -31,7 +31,6 @@ import type {
|
||||
} from "../AnalysisProvider.js";
|
||||
import {
|
||||
AnalysisSnapshotCache,
|
||||
SNAPSHOT_CACHE_ENTRY_CEILING,
|
||||
isSnapshotCacheable,
|
||||
isSourceEvidenceRetainable,
|
||||
} from "./AnalysisSnapshotCache.js";
|
||||
@@ -595,7 +594,7 @@ describe("analysis snapshot cache capacity", () => {
|
||||
parameters: { query: "existing" },
|
||||
execution,
|
||||
};
|
||||
for (let index = 0; index < SNAPSHOT_CACHE_ENTRY_CEILING - 1; index += 1)
|
||||
for (let index = 0; index < 9_999; index += 1)
|
||||
cache.recordWorkflow({
|
||||
...workflowInput,
|
||||
parameters: { query: String(index) },
|
||||
@@ -620,9 +619,7 @@ describe("analysis snapshot cache capacity", () => {
|
||||
});
|
||||
cache.recordWorkflow(workflowInput);
|
||||
expect(cache.entries()).toHaveLength(1);
|
||||
expect(cache.workflowEntries()).toHaveLength(
|
||||
SNAPSHOT_CACHE_ENTRY_CEILING - 1,
|
||||
);
|
||||
expect(cache.workflowEntries()).toHaveLength(9_999);
|
||||
|
||||
cache.record({
|
||||
...directInput,
|
||||
@@ -669,9 +666,7 @@ describe("analysis snapshot cache capacity", () => {
|
||||
),
|
||||
"a capacity-refused workflow binding is not counted",
|
||||
).toBe(0);
|
||||
expect(cache.workflowEntries()).toHaveLength(
|
||||
SNAPSHOT_CACHE_ENTRY_CEILING - 1,
|
||||
);
|
||||
expect(cache.workflowEntries()).toHaveLength(9_999);
|
||||
|
||||
const otherTarget = { ...target, sha256: "b".repeat(64) };
|
||||
cache.recordWorkflow({
|
||||
|
||||
@@ -87,7 +87,7 @@ const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
);
|
||||
|
||||
/** Maximum duplicated result bindings retained across direct and workflow queries. */
|
||||
export const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000;
|
||||
const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000;
|
||||
|
||||
const replayDocumentSelected = (
|
||||
operation: AnalysisOperation,
|
||||
|
||||
@@ -1754,22 +1754,16 @@ export class BinarySession
|
||||
this.#transitionPending += 1;
|
||||
// Later calls wait for this transition and must not be included in its drain.
|
||||
const admittedThrough = this.#transitionGeneration++;
|
||||
const result = this.#transition.then(
|
||||
() => operation(admittedThrough),
|
||||
() => operation(admittedThrough),
|
||||
);
|
||||
const perform = (): Promise<T> => operation(admittedThrough);
|
||||
const result = this.#transition.then(perform, perform);
|
||||
this.#transition = result.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
void result.then(
|
||||
() => {
|
||||
this.#transitionPending -= 1;
|
||||
},
|
||||
() => {
|
||||
this.#transitionPending -= 1;
|
||||
},
|
||||
);
|
||||
const finish = (): void => {
|
||||
this.#transitionPending -= 1;
|
||||
};
|
||||
void result.then(finish, finish);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1864,32 +1858,19 @@ export class BinarySession
|
||||
);
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
transition.then(
|
||||
() => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(
|
||||
signal.aborted
|
||||
? err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: signal.reason,
|
||||
}),
|
||||
)
|
||||
: ok(undefined),
|
||||
);
|
||||
},
|
||||
() => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(
|
||||
signal.aborted
|
||||
? err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: signal.reason,
|
||||
}),
|
||||
)
|
||||
: ok(undefined),
|
||||
);
|
||||
},
|
||||
);
|
||||
const finish = (): void => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(
|
||||
signal.aborted
|
||||
? err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: signal.reason,
|
||||
}),
|
||||
)
|
||||
: ok(undefined),
|
||||
);
|
||||
};
|
||||
transition.then(finish, finish);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { parseAppleIps } from "../../native/apple/AppleIpsParser.js";
|
||||
import { inspectAppleIpsInputSchema } from "../../domain/native/appleIps.js";
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { AppleIpsService } from "./AppleIpsService.js";
|
||||
@@ -10,52 +13,19 @@ import {
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
|
||||
const identity = APPLE_IPS_PROVIDER;
|
||||
const report: AppleIps = {
|
||||
artifact: {
|
||||
path: "/artifacts/report.ips",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 128,
|
||||
const bytes = Buffer.from(
|
||||
'{"bug_type":"309"}\n{"procName":"rea-crash-fixture","threads":[],"usedImages":[]}',
|
||||
);
|
||||
const path = "/artifacts/report.ips";
|
||||
const report = parseAppleIps(
|
||||
bytes,
|
||||
{
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
metadata: {
|
||||
bug_type: "309",
|
||||
timestamp: null,
|
||||
os_version: null,
|
||||
bundle_id: null,
|
||||
incident_id: null,
|
||||
name: null,
|
||||
build_version: null,
|
||||
platform: null,
|
||||
unlisted_keys: [],
|
||||
},
|
||||
process: {
|
||||
name: "rea-crash-fixture",
|
||||
path: null,
|
||||
pid: null,
|
||||
parent: null,
|
||||
parent_pid: null,
|
||||
cpu_type: null,
|
||||
translated: null,
|
||||
os_version: null,
|
||||
},
|
||||
exception: null,
|
||||
termination: null,
|
||||
faulting_thread: null,
|
||||
threads: [],
|
||||
images: [],
|
||||
classification: { status: "unclassified", matches: [] },
|
||||
coverage: {
|
||||
status: "complete",
|
||||
reason: null,
|
||||
threads: 0,
|
||||
frames: 0,
|
||||
images: 0,
|
||||
registers: 0,
|
||||
type_mismatches: [],
|
||||
},
|
||||
unlisted_body_keys: [],
|
||||
join: null,
|
||||
limitations: ["Opened Mach-O and dSYM UUID joins remain unobserved."],
|
||||
};
|
||||
inspectAppleIpsInputSchema.parse({ path }),
|
||||
);
|
||||
|
||||
it.each([0, 5])(
|
||||
"rejects an unresolved faulting thread %s without its mismatch",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type {
|
||||
ExecutionOptions,
|
||||
ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
BinaryLayout,
|
||||
InspectBinaryLayoutInput,
|
||||
} from "../../domain/native/binaryLayout.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
import type { ProviderIdentity } from "../AnalysisProvider.js";
|
||||
|
||||
/** Replaceable offline binary-layout provider; owns its parser and process protocol. */
|
||||
export interface BinaryLayoutPort {
|
||||
|
||||
@@ -5,10 +5,7 @@ import { expect, it, vi } from "vitest";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { byteEntropySchema } from "../../domain/native/byteEntropy.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
ByteEntropyService,
|
||||
MAX_BYTE_ENTROPY_BLOCKS,
|
||||
} from "./ByteEntropyService.js";
|
||||
import { ByteEntropyService } from "./ByteEntropyService.js";
|
||||
|
||||
it("keeps whole-file identity and entropy while selecting unaligned blocks and a partial tail", async () => {
|
||||
const root = await createTestTempDirectory("rea-entropy-");
|
||||
@@ -421,7 +418,7 @@ it("reports a changed artifact when its parent directory moves during streaming"
|
||||
it("refuses a block series above the output-row limit before scanning", async () => {
|
||||
const root = await createTestTempDirectory("rea-entropy-block-limit-");
|
||||
const path = join(root, "bytes");
|
||||
await writeFile(path, Buffer.alloc(MAX_BYTE_ENTROPY_BLOCKS * 256 + 1));
|
||||
await writeFile(path, Buffer.alloc(32_768 * 256 + 1));
|
||||
let reads = 0;
|
||||
const service = new ByteEntropyService({
|
||||
stat,
|
||||
@@ -443,8 +440,8 @@ it("refuses a block series above the output-row limit before scanning", async ()
|
||||
_tag: "AnalysisResourceConstraintError",
|
||||
resource: "capacity",
|
||||
reportedLimits: {
|
||||
max_blocks: MAX_BYTE_ENTROPY_BLOCKS,
|
||||
requested_blocks: MAX_BYTE_ENTROPY_BLOCKS + 1,
|
||||
max_blocks: 32_768,
|
||||
requested_blocks: 32_769,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
import { parseAnalysisInput } from "../AnalysisInput.js";
|
||||
|
||||
/** Bound retained result rows before constructing the Evidence representation. */
|
||||
export const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
|
||||
const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
|
||||
|
||||
/** Generated files such as Linux /proc entries report size 0 but still hold bytes. */
|
||||
const ZERO_SIZE_LIMITATION =
|
||||
|
||||
@@ -2,7 +2,10 @@ import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { ElfNotesService } from "./ElfNotesService.js";
|
||||
import { ELF_NOTES_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
import type { ElfNotes } from "../../domain/native/elfNotes.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { inspectElfNotesInputSchema } from "../../domain/native/elfNotes.js";
|
||||
import { elfNoteFixture } from "../../native/elf/ElfNotes.fixture.js";
|
||||
import { parseElfNotes } from "../../native/elf/ElfNoteParser.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
@@ -11,22 +14,17 @@ import {
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
|
||||
const identity = ELF_NOTES_PROVIDER;
|
||||
const report: ElfNotes = {
|
||||
artifact: {
|
||||
path: "/artifacts/app",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 512,
|
||||
const path = "/artifacts/app";
|
||||
const bytes = elfNoteFixture({ ownership: "none" });
|
||||
const report = parseElfNotes(
|
||||
bytes,
|
||||
{
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
format: "elf64",
|
||||
byte_order: "little",
|
||||
machine: 62,
|
||||
type: 3,
|
||||
type_name: "shared",
|
||||
tables: [],
|
||||
entries: [],
|
||||
coverage: { status: "complete", examined_tables: 0, examined_entries: 0 },
|
||||
limitations: ["No PT_NOTE program header or SHT_NOTE section was declared."],
|
||||
};
|
||||
inspectElfNotesInputSchema.parse({ path }),
|
||||
);
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
|
||||
@@ -27,57 +27,6 @@ const undecodableHeaderPointer = (): Buffer => {
|
||||
bytes[Number(bytes.readBigUInt64LE(64 + 56 + 8)) + 1] = 0x0f;
|
||||
return bytes;
|
||||
};
|
||||
const report: ElfUnwindRanges = {
|
||||
artifact: {
|
||||
path: "/artifacts/app",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 512,
|
||||
},
|
||||
format: "elf64",
|
||||
byte_order: "little",
|
||||
machine: 62,
|
||||
type: 3,
|
||||
type_name: "shared",
|
||||
eh_frame: { status: "absent", source: null, location: null },
|
||||
eh_frame_hdr: {
|
||||
status: "absent",
|
||||
location: null,
|
||||
version: null,
|
||||
eh_frame_pointer_encoding: null,
|
||||
fde_count_encoding: null,
|
||||
table_encoding: null,
|
||||
eh_frame_pointer: null,
|
||||
fde_count: null,
|
||||
table_entries: 0,
|
||||
},
|
||||
debug_frame: { status: "absent", location: null },
|
||||
cies: [],
|
||||
ranges: [],
|
||||
page: {
|
||||
offset: 0,
|
||||
limit: 256,
|
||||
returned: 0,
|
||||
total: 0,
|
||||
next_offset: null,
|
||||
},
|
||||
coverage: {
|
||||
status: "complete",
|
||||
total_ranges: 0,
|
||||
total_cies: 0,
|
||||
ranges_with_symbols: 0,
|
||||
ranges_without_symbols: 0,
|
||||
eh_frame_hdr_agreement: "unavailable",
|
||||
examined_records: 0,
|
||||
},
|
||||
limitations: [
|
||||
"No .eh_frame section or PT_GNU_EH_FRAME segment was declared.",
|
||||
"No .eh_frame_hdr section or PT_GNU_EH_FRAME segment was declared.",
|
||||
"CFA instructions are counted but not evaluated.",
|
||||
".debug_frame is absent; only .eh_frame was examined.",
|
||||
"Symbol names, section names, and augmentation strings are target-derived and untrusted.",
|
||||
],
|
||||
};
|
||||
|
||||
const parsed = (
|
||||
bytes: Buffer,
|
||||
raw: Readonly<Record<string, unknown>> = {},
|
||||
@@ -97,6 +46,8 @@ const parsed = (
|
||||
);
|
||||
};
|
||||
|
||||
const report = parsed(elfUnwindAbsentFixture());
|
||||
|
||||
const accept = async (
|
||||
value: ElfUnwindRanges,
|
||||
raw: Readonly<Record<string, unknown>> = {},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { inspectPeCertificatesInputSchema } from "../../domain/native/peCertificates.js";
|
||||
import { peCertificateFixture } from "../../native/pe/PeCertificates.fixture.js";
|
||||
import {
|
||||
peCertificateFixture,
|
||||
peWithoutCertificateTableFixture,
|
||||
} from "../../native/pe/PeCertificates.fixture.js";
|
||||
import { parsePeCertificates } from "../../native/pe/PeCertificateParser.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { PeCertificatesService } from "./PeCertificatesService.js";
|
||||
@@ -15,19 +18,17 @@ import {
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
|
||||
const identity = PE_CERTIFICATES_PROVIDER;
|
||||
const report: PeCertificates = {
|
||||
artifact: {
|
||||
path: "/artifacts/app.exe",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 512,
|
||||
const path = "/artifacts/app.exe";
|
||||
const bytes = peWithoutCertificateTableFixture();
|
||||
const report = parsePeCertificates(
|
||||
bytes,
|
||||
{
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
format: "pe32",
|
||||
machine: 0x14c,
|
||||
table: null,
|
||||
entries: [],
|
||||
coverage: { status: "complete", examined_entries: 0, certificates: 0 },
|
||||
limitations: ["IMAGE_DIRECTORY_ENTRY_SECURITY is absent."],
|
||||
};
|
||||
inspectPeCertificatesInputSchema.parse({ path }),
|
||||
);
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { peWithoutImportTableFixture } from "../../../tests/fixtures/binaryDiagnostics/peImports.js";
|
||||
import { parsePeImports } from "../../native/pe/PeImportParser.js";
|
||||
import {
|
||||
inspectPeImportsInputSchema,
|
||||
peImportsSchema,
|
||||
type PeImports,
|
||||
} from "../../domain/native/peImports.js";
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { PeImportsService } from "./PeImportsService.js";
|
||||
import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
import type { PeImports } from "../../domain/native/peImports.js";
|
||||
import { peImportsSchema } from "../../domain/native/peImports.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
@@ -12,35 +18,17 @@ import {
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
|
||||
const identity = PE_IMPORTS_PROVIDER;
|
||||
const report: PeImports = {
|
||||
artifact: {
|
||||
path: "/artifacts/app.exe",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 512,
|
||||
const path = "/artifacts/app.exe";
|
||||
const bytes = peWithoutImportTableFixture();
|
||||
const report = await parsePeImports(
|
||||
bytes,
|
||||
{
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
format: "pe32-plus",
|
||||
machine: 0x8664,
|
||||
bits: 64,
|
||||
architecture: "amd64",
|
||||
import_directory: null,
|
||||
delay_import_directory: null,
|
||||
imports: [],
|
||||
delay_imports: [],
|
||||
imphash: {
|
||||
algorithm: "pefile",
|
||||
md5: null,
|
||||
reason: "IMAGE_DIRECTORY_ENTRY_IMPORT is absent.",
|
||||
},
|
||||
coverage: {
|
||||
status: "complete",
|
||||
reason: null,
|
||||
examined_dlls: 0,
|
||||
imports: 0,
|
||||
delay_imports: 0,
|
||||
},
|
||||
issues: [],
|
||||
limitations: ["IMAGE_DIRECTORY_ENTRY_IMPORT is absent."],
|
||||
};
|
||||
inspectPeImportsInputSchema.parse({ path }),
|
||||
);
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
@@ -189,7 +177,7 @@ it("publishes every consumed directory span as its own Evidence location", async
|
||||
size: 40,
|
||||
locations: [
|
||||
{ offset: 0x100, bytes: 20 },
|
||||
{ offset: 0x1f8, bytes: 20 },
|
||||
{ offset: report.artifact.bytes - 8, bytes: 20 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type PeResourceIdentity,
|
||||
} from "../../domain/native/peResources.js";
|
||||
import { peSubjectArchitecture } from "../../domain/peMachine.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
import { parseAnalysisInput } from "../AnalysisInput.js";
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { WindowsShortcutService } from "./WindowsShortcutService.js";
|
||||
import { WINDOWS_SHORTCUT_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
import type { WindowsShortcut } from "../../domain/native/windowsShortcut.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { inspectWindowsShortcutInputSchema } from "../../domain/native/windowsShortcut.js";
|
||||
import { parseWindowsShortcut } from "../../native/windows/WindowsShortcutParser.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
@@ -12,64 +14,20 @@ import {
|
||||
|
||||
const identity = WINDOWS_SHORTCUT_PROVIDER;
|
||||
|
||||
const report: WindowsShortcut = {
|
||||
artifact: {
|
||||
path: "/artifacts/shortcut.lnk",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 128,
|
||||
const path = "/artifacts/shortcut.lnk";
|
||||
const bytes = Buffer.alloc(80);
|
||||
bytes.writeUInt32LE(76, 0);
|
||||
Buffer.from("0114020000000000c000000000000046", "hex").copy(bytes, 4);
|
||||
bytes.writeUInt32LE(1, 60);
|
||||
const report = await parseWindowsShortcut(
|
||||
bytes,
|
||||
{
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
header: {
|
||||
location: { offset: 0, bytes: 76 },
|
||||
header_size: 76,
|
||||
clsid: "00021401-0000-0000-C000-000000000046",
|
||||
link_flags: { raw: 0, decoded: [] },
|
||||
file_attributes: { raw: 0, decoded: [] },
|
||||
creation_time: { raw: "0", iso8601: null },
|
||||
access_time: { raw: "0", iso8601: null },
|
||||
write_time: { raw: "0", iso8601: null },
|
||||
file_size: 0,
|
||||
icon_index: 0,
|
||||
show_command: { raw: 1, decoded: "normal" },
|
||||
hotkey: {
|
||||
raw: 0,
|
||||
modifiers: [],
|
||||
virtual_key: null,
|
||||
virtual_key_name: null,
|
||||
},
|
||||
},
|
||||
id_list: null,
|
||||
link_info: null,
|
||||
string_data: {
|
||||
name: null,
|
||||
relative_path: null,
|
||||
working_dir: null,
|
||||
arguments: null,
|
||||
icon_location: null,
|
||||
},
|
||||
extra_data: { blocks: [], terminal_offset: null },
|
||||
observations: [
|
||||
{ kind: "arguments_present", evidence_kind: "derived", value: false },
|
||||
{ kind: "argument_length", evidence_kind: "derived", characters: 0 },
|
||||
{ kind: "target_extension", evidence_kind: "derived", extension: null },
|
||||
{ kind: "show_command_minimized", evidence_kind: "derived", value: false },
|
||||
{
|
||||
kind: "icon_location_differs_from_target",
|
||||
evidence_kind: "derived",
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
coverage: {
|
||||
status: "complete",
|
||||
reason: null,
|
||||
examined_id_list_items: 0,
|
||||
examined_extra_data_blocks: 0,
|
||||
failing_offset: null,
|
||||
},
|
||||
issues: [],
|
||||
limitations: [
|
||||
"Rows reproduce the selected Shell Link bytes; they do not resolve, open, or execute the link target.",
|
||||
],
|
||||
};
|
||||
inspectWindowsShortcutInputSchema.parse({ path }),
|
||||
);
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
READER_RUNNER_SLACK_S,
|
||||
readerTimeout,
|
||||
} from "../../domain/desktop/appTaskReaders.js";
|
||||
import { BUILD_REA_TIMEOUT_S } from "../../desktop/BuildComputerController.js";
|
||||
import { BuildComputerLongRuns } from "../../desktop/BuildComputerLongRuns.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
@@ -32,6 +31,7 @@ import { jsonValueSchema, type JsonValue } from "../../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { AGENTENV_PROVIDER } from "../../desktop/AgentEnvComputerHelper.js";
|
||||
import {
|
||||
BUILD_REA_TIMEOUT_S,
|
||||
fetchHomeFolder,
|
||||
type ControlledComputer,
|
||||
} from "../../desktop/BuildComputerController.js";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../domain/electron/electronFuses.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { createElectronFuseEvidence } from "../../domain/electron/electronFuseEvidence.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
import { parseAnalysisInput } from "../AnalysisInput.js";
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
TEMPLATE_RULES,
|
||||
type TemplateRule,
|
||||
} from "../../domain/analysisView/environmentManifestRules.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import type { EnvironmentBuildReport } from "../../domain/environmentBundleExport.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
|
||||
import { z } from "zod";
|
||||
import { basename } from "node:path";
|
||||
import { isIP } from "node:net";
|
||||
@@ -83,9 +84,6 @@ const SCAN_BODY_LIMIT_BYTES = 4 * 1024 * 1024;
|
||||
/** Pointers the report lists; the count covers every match. */
|
||||
const REPORTED_POINTERS = 20;
|
||||
|
||||
const pointerToken = (name: string): string =>
|
||||
name.replaceAll("~", "~0").replaceAll("/", "~1");
|
||||
|
||||
/** A held value: a non-empty string, or an array holding one. */
|
||||
const holdsValue = (value: unknown): boolean =>
|
||||
(typeof value === "string" && value !== "") ||
|
||||
@@ -198,7 +196,7 @@ const credentialTally = () => {
|
||||
const entries = Object.entries(value);
|
||||
for (let index = entries.length - 1; index >= 0; index--) {
|
||||
const [name, child] = entries[index]!;
|
||||
const path = `${pointer}/${pointerToken(name)}`;
|
||||
const path = `${pointer}/${escapeJsonPointerToken(name)}`;
|
||||
field(`${label} ${path}`, name, child);
|
||||
stack.push([child, path]);
|
||||
}
|
||||
@@ -209,7 +207,7 @@ const credentialTally = () => {
|
||||
else if (scanned?.kind === "json") walk(scanned.value, label);
|
||||
else if (scanned?.kind === "form")
|
||||
for (const [name, value] of scanned.fields)
|
||||
field(`${label} /${pointerToken(name)}`, name, value);
|
||||
field(`${label} /${escapeJsonPointerToken(name)}`, name, value);
|
||||
};
|
||||
return {
|
||||
field,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
|
||||
import type { JsonObject } from "../../domain/analysisView/environmentStateRules.js";
|
||||
import type { FieldRule } from "../../domain/analysisView/environmentStateWriteRules.js";
|
||||
import { matchesClockFormat } from "../../domain/analysisView/environmentStateWriteClockFormats.js";
|
||||
@@ -74,9 +75,6 @@ const shown = (value: JsonValue | undefined): string => {
|
||||
return text.length > VALUE_CHARS ? `${text.slice(0, VALUE_CHARS)}…` : text;
|
||||
};
|
||||
|
||||
const escapePointer = (key: string): string =>
|
||||
key.replaceAll("~", "~0").replaceAll("/", "~1");
|
||||
|
||||
const formatKey = (format: ClockFormat): string =>
|
||||
format.kind === "epoch"
|
||||
? `epoch-${format.unit}`
|
||||
@@ -427,7 +425,7 @@ class Differ {
|
||||
? { agreed: new Set<string>(), fields: new Map<string, string>() }
|
||||
: undefined;
|
||||
for (const [key, value] of Object.entries(live)) {
|
||||
const child = `${pointer}/${escapePointer(key)}`;
|
||||
const child = `${pointer}/${escapeJsonPointerToken(key)}`;
|
||||
const other = envKey(key);
|
||||
// A translated key and a verbatim key may name the same entry; an
|
||||
// environment member can account for only one live member.
|
||||
@@ -460,7 +458,7 @@ class Differ {
|
||||
? undefined
|
||||
: {
|
||||
...generation,
|
||||
pointer: `${generation.pointer}/${escapePointer(key)}`,
|
||||
pointer: `${generation.pointer}/${escapeJsonPointerToken(key)}`,
|
||||
},
|
||||
);
|
||||
if (this.differences.length === before) composed?.agreed.add(key);
|
||||
@@ -489,7 +487,7 @@ class Differ {
|
||||
} else
|
||||
this.add({
|
||||
category: "value",
|
||||
pointer: `${pointer}/${escapePointer(rule.field)}`,
|
||||
pointer: `${pointer}/${escapeJsonPointerToken(rule.field)}`,
|
||||
live: shown(live[rule.field]),
|
||||
env: shown(env[other]),
|
||||
});
|
||||
@@ -499,7 +497,7 @@ class Differ {
|
||||
if (!matched.has(key))
|
||||
this.add({
|
||||
category: "extra-field",
|
||||
pointer: `${pointer}/${escapePointer(key)}`,
|
||||
pointer: `${pointer}/${escapeJsonPointerToken(key)}`,
|
||||
env: shown(value),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
|
||||
import type { JsonObject } from "../../domain/analysisView/environmentStateRules.js";
|
||||
import type { BodyAddress } from "../../domain/analysisView/environmentStateWriteRules.js";
|
||||
import { isUuid } from "../../domain/analysisView/environmentStateWriteValues.js";
|
||||
@@ -315,7 +316,7 @@ const idPair = (
|
||||
: undefined;
|
||||
|
||||
const childPointer = (at: string, key: string | number): string =>
|
||||
`${at}/${String(key).replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
||||
`${at}/${escapeJsonPointerToken(String(key))}`;
|
||||
|
||||
const learnAt = (
|
||||
live: JsonValue | undefined,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { jsonPointerTokens } from "../../domain/jsonPointer.js";
|
||||
import {
|
||||
createdFields,
|
||||
documentBodyProblem,
|
||||
@@ -53,7 +54,6 @@ import {
|
||||
collectionMatch,
|
||||
decodedSegment,
|
||||
isObject,
|
||||
pointerTokens,
|
||||
scalarText,
|
||||
type JsonObject,
|
||||
type StateEntity,
|
||||
@@ -435,7 +435,7 @@ export const writeAnswer = (
|
||||
// A member the call sent and the plan stores as sent is answered as sent; any other keeps the recorded answer.
|
||||
unloaded: (plan.observation?.read?.unloaded ?? []).filter(
|
||||
(path) => {
|
||||
const [field] = pointerTokens(path);
|
||||
const [field] = jsonPointerTokens(path);
|
||||
return !(
|
||||
rendered.sent?.includes(field!) === true &&
|
||||
plan.rules.some(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { jsonPointerTokens } from "../../domain/jsonPointer.js";
|
||||
import {
|
||||
ownMember,
|
||||
isObject,
|
||||
@@ -5,7 +6,6 @@ import {
|
||||
isSafeHttpMethod,
|
||||
listItems,
|
||||
locateList,
|
||||
pointerTokens,
|
||||
type JsonObject,
|
||||
} from "../../domain/analysisView/environmentStateRules.js";
|
||||
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
||||
@@ -170,7 +170,7 @@ const describeJson = (value: JsonValue): string => {
|
||||
const pointer = locateList(value, undefined);
|
||||
const items = pointer === undefined ? [] : (listItems(value, pointer) ?? []);
|
||||
if (pointer !== undefined && items.length > 0) {
|
||||
const [member] = pointerTokens(pointer);
|
||||
const [member] = jsonPointerTokens(pointer);
|
||||
return member === undefined
|
||||
? `a JSON array of objects ${withFields(items)}`
|
||||
: `a JSON object whose ${describeText(member, "member", "capture Evidence", JSON.stringify)} lists objects ${withFields(items)}`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
|
||||
import type { AnalysisSettingsReader } from "../../domain/analysisSettings.js";
|
||||
import {
|
||||
ASAR_HEADER_ADMISSION_SETTING,
|
||||
@@ -145,7 +146,8 @@ export const analyzeJavaScriptApplicationValidated = async (
|
||||
});
|
||||
assertJavaScriptAnalysisActive(options.signal);
|
||||
return ok(evidence);
|
||||
} catch (cause: unknown) {
|
||||
} catch (caught: unknown) {
|
||||
let cause = caught;
|
||||
if (
|
||||
cause instanceof ArtifactReaderFailure &&
|
||||
cause.reason === "cancelled" &&
|
||||
@@ -246,13 +248,10 @@ const describeResultSchemaFailure = (cause: z.ZodError): string => {
|
||||
const pointer =
|
||||
path.length === 0
|
||||
? "/"
|
||||
: `/${path.map((part) => escapePointerPart(String(part))).join("/")}`;
|
||||
: `/${path.map((part) => escapeJsonPointerToken(String(part))).join("/")}`;
|
||||
return `Result schema rejected ${String(cause.issues.length)} ${cause.issues.length === 1 ? "issue" : "issues"} at ${pointer} (${issue.code})`;
|
||||
};
|
||||
|
||||
const escapePointerPart = (part: string): string =>
|
||||
part.replaceAll("~", "~0").replaceAll("/", "~1");
|
||||
|
||||
const FILESYSTEM_ERROR_CODES = new Set([
|
||||
"ENOENT",
|
||||
"EACCES",
|
||||
|
||||
@@ -214,8 +214,11 @@ export const reconstructJavaScriptArtifact = async (
|
||||
).length,
|
||||
{ scope },
|
||||
);
|
||||
let outcome;
|
||||
let cleanupFailure: AnalysisError | undefined;
|
||||
let outcome!: Awaited<
|
||||
ReturnType<typeof analyzeAndAdoptJavaScriptArtifactFiles>
|
||||
>;
|
||||
let analysisFailure: { readonly cause: unknown } | undefined;
|
||||
let cleanupAttempt: Awaited<ReturnType<ArtifactResourceScope["release"]>>;
|
||||
try {
|
||||
outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
|
||||
files,
|
||||
@@ -250,18 +253,28 @@ export const reconstructJavaScriptArtifact = async (
|
||||
preloads.paths,
|
||||
preloads.limitations,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
analysisFailure = { cause };
|
||||
} finally {
|
||||
try {
|
||||
const cleanup = await scope.release({
|
||||
kind: "javascript-source-analysis",
|
||||
analysis: worker,
|
||||
resource: `JavaScript source analyzer for ${path}`,
|
||||
});
|
||||
if (cleanup.kind === "failed") throw cleanup.cause;
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof AnalysisError)) throw cause;
|
||||
cleanupFailure = cause;
|
||||
}
|
||||
cleanupAttempt = await scope.release({
|
||||
kind: "javascript-source-analysis",
|
||||
analysis: worker,
|
||||
resource: `JavaScript source analyzer for ${path}`,
|
||||
});
|
||||
}
|
||||
if (analysisFailure !== undefined) {
|
||||
if (cleanupAttempt.kind === "failed")
|
||||
throw scope.cleanupFailure(
|
||||
{ kind: "failed", cause: analysisFailure.cause },
|
||||
[cleanupAttempt],
|
||||
);
|
||||
throw analysisFailure.cause;
|
||||
}
|
||||
let cleanupFailure: AnalysisError | undefined;
|
||||
if (cleanupAttempt.kind === "failed") {
|
||||
if (!(cleanupAttempt.cause instanceof AnalysisError))
|
||||
throw cleanupAttempt.cause;
|
||||
cleanupFailure = cleanupAttempt.cause;
|
||||
}
|
||||
const analysis = outcome.analysis;
|
||||
let interruption = outcome.interruption;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { BrowserObservationError } from "../../domain/browserObservationError.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import type { JavaScriptRuntimeTargetList } from "../../domain/javascript/javascriptRuntimeObservation.js";
|
||||
|
||||
@@ -16,6 +16,7 @@ import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { ntfsJournalResultSchema } from "../../domain/windows/ntfsJournalQuery.js";
|
||||
import { inspectNtfsJournalEvidence } from "./NtfsJournalService.js";
|
||||
import { createDeferred } from "../../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
|
||||
const acquisitions = async () => {
|
||||
@@ -170,7 +171,11 @@ it("keeps an active acquisition independent of a cancelled peer, bounds concurre
|
||||
});
|
||||
try {
|
||||
const active = inspectNtfsJournalEvidence(fixture.input, resources);
|
||||
await entered.promise;
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
active,
|
||||
"provider lifecycle boundary",
|
||||
);
|
||||
const cancelled = new AbortController();
|
||||
cancelled.abort("peer cancelled");
|
||||
const peer = await inspectNtfsJournalEvidence(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { open, writeFile, type FileHandle } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { ArtifactFileSource } from "./ArtifactFileSource.js";
|
||||
|
||||
it("retries an admission cancelled after opening and closing the selected descriptor", async () => {
|
||||
@@ -132,7 +133,7 @@ it("lets another caller continue when shared admission is cancelled", async () =
|
||||
},
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
await started;
|
||||
await awaitAcknowledgement(started, first, "artifact source admission");
|
||||
const second = source.read();
|
||||
await Promise.resolve();
|
||||
controller.abort(reason);
|
||||
|
||||
@@ -5,6 +5,17 @@ import { translateArtifactFailure } from "./ArtifactOperationFailure.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { artifactReaderLimitFailure } from "./ArtifactReaderLimits.js";
|
||||
import { constants as bufferConstants } from "node:buffer";
|
||||
import { imageFileSizeRemediation } from "./StableArtifactFailure.js";
|
||||
|
||||
// The provider ceiling test uses a sparse file and is skipped on Windows.
|
||||
it.skipIf(process.platform !== "win32")(
|
||||
"names the fixed image ceiling when the selected budget is already maximal",
|
||||
() => {
|
||||
expect(imageFileSizeRemediation(1024, 1024)).toContain(
|
||||
"no runtime override",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ effective: 1, required: bufferConstants.MAX_STRING_LENGTH + 1 },
|
||||
@@ -94,6 +105,18 @@ it.each(["capacity", "file-size", "memory"] as const)(
|
||||
message: failure.message,
|
||||
details: { boundary, detail: failure.message },
|
||||
});
|
||||
const wrapped = ArtifactReaderFailure.withCleanup(failure, {
|
||||
reason: "close failed",
|
||||
resources: ["handle"],
|
||||
});
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
translateArtifactFailure("inventory_artifact", wrapped),
|
||||
),
|
||||
).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
details: { boundary, cleanup: "incomplete", resources: ["handle"] },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { open, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import {
|
||||
withArtifactFileHandle,
|
||||
bindArtifactFileCleanup,
|
||||
@@ -36,25 +37,33 @@ it("waits for admitted operations and shares concurrent close", async () => {
|
||||
started.resolve();
|
||||
await finish.promise;
|
||||
});
|
||||
await started.promise;
|
||||
try {
|
||||
await awaitAcknowledgement(started.promise, operation, "scope admission");
|
||||
const close = scope.close();
|
||||
expect(scope.close()).toBe(close);
|
||||
await expect(scope.run(async () => undefined)).rejects.toMatchObject({
|
||||
reason: "unavailable",
|
||||
message: expect.stringContaining("closed"),
|
||||
});
|
||||
let closed = false;
|
||||
void close.then(() => {
|
||||
closed = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(closed).toBe(false);
|
||||
|
||||
const close = scope.close();
|
||||
expect(scope.close()).toBe(close);
|
||||
await expect(scope.run(async () => undefined)).rejects.toMatchObject({
|
||||
reason: "unavailable",
|
||||
message: expect.stringContaining("closed"),
|
||||
});
|
||||
let closed = false;
|
||||
void close.then(() => {
|
||||
closed = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(closed).toBe(false);
|
||||
|
||||
finish.resolve();
|
||||
await operation;
|
||||
await close;
|
||||
expect(closed).toBe(true);
|
||||
finish.resolve();
|
||||
await operation;
|
||||
await close;
|
||||
expect(closed).toBe(true);
|
||||
} finally {
|
||||
finish.resolve();
|
||||
try {
|
||||
await operation;
|
||||
} finally {
|
||||
await scope.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("stops a close hook even when retained cleanup is still failing", async () => {
|
||||
|
||||
@@ -89,7 +89,13 @@ it("fails closed on over-limit records", async () => {
|
||||
scanCanonicalArtifactInventory(
|
||||
await writeImage("runs.hex", `${runs.join("\n")}\n`),
|
||||
),
|
||||
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
message: expect.stringContaining(
|
||||
"more than 65536 address-discontiguous record runs",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves an invalid first record to plain file classification", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter, once } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { buffer } from "node:stream/consumers";
|
||||
import { truncate, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -14,6 +15,7 @@ import { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
|
||||
import type { NativeCommandRunner } from "../native/CommandRunner.js";
|
||||
import { NativeCommandFailure } from "../native/CommandRunner.js";
|
||||
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
import type { ArtifactEntry } from "./ArtifactReader.js";
|
||||
|
||||
@@ -157,56 +159,60 @@ describe("Mach-O command cleanup lifecycle", () => {
|
||||
const reader = new MachOSliceArtifactReader(binary, {}, runner);
|
||||
const iteration = reader.entries();
|
||||
const firstEntry = iteration[Symbol.asyncIterator]().next();
|
||||
await started;
|
||||
const closing = reader.close();
|
||||
await Promise.resolve();
|
||||
expect(cleanupRetries).toBe(0);
|
||||
finishCommand(
|
||||
err(
|
||||
new NativeCommandFailure(
|
||||
"lipo",
|
||||
"timeout",
|
||||
null,
|
||||
undefined,
|
||||
{
|
||||
stdout: "partial lipo output",
|
||||
stderr: "partial lipo diagnostic",
|
||||
stdoutBytes: 19,
|
||||
stderrBytes: 23,
|
||||
exitCode: null,
|
||||
signal: null,
|
||||
truncated: false,
|
||||
},
|
||||
"capture process group remains unverified",
|
||||
["capture-group:9999"],
|
||||
),
|
||||
const commandResult = err(
|
||||
new NativeCommandFailure(
|
||||
"lipo",
|
||||
"timeout",
|
||||
null,
|
||||
undefined,
|
||||
{
|
||||
stdout: "partial lipo output",
|
||||
stderr: "partial lipo diagnostic",
|
||||
stdoutBytes: 19,
|
||||
stderrBytes: 23,
|
||||
exitCode: null,
|
||||
signal: null,
|
||||
truncated: false,
|
||||
},
|
||||
"capture process group remains unverified",
|
||||
["capture-group:9999"],
|
||||
),
|
||||
);
|
||||
|
||||
await expect(firstEntry).rejects.toMatchObject({
|
||||
reason: "unavailable",
|
||||
cause: expect.objectContaining({
|
||||
cleanupFailure: "capture process group remains unverified",
|
||||
}),
|
||||
cleanup: {
|
||||
reason: "capture process group remains unverified",
|
||||
resources: ["capture-group:9999"],
|
||||
},
|
||||
});
|
||||
await expect(closing).rejects.toMatchObject({
|
||||
cleanup: {
|
||||
reason:
|
||||
"run:first, process-group:1234: first process group remains live; run:second, process-group:5678: second process group remains live",
|
||||
resources: [
|
||||
"run:first",
|
||||
"process-group:1234",
|
||||
"run:second",
|
||||
"process-group:5678",
|
||||
],
|
||||
},
|
||||
});
|
||||
await reader.close();
|
||||
expect(cleanupRetries).toBe(2);
|
||||
try {
|
||||
await awaitAcknowledgement(started, firstEntry, "lipo capture start");
|
||||
const closing = reader.close();
|
||||
await Promise.resolve();
|
||||
expect(cleanupRetries).toBe(0);
|
||||
finishCommand(commandResult);
|
||||
await expect(firstEntry).rejects.toMatchObject({
|
||||
reason: "unavailable",
|
||||
cause: expect.objectContaining({
|
||||
cleanupFailure: "capture process group remains unverified",
|
||||
}),
|
||||
cleanup: {
|
||||
reason: "capture process group remains unverified",
|
||||
resources: ["capture-group:9999"],
|
||||
},
|
||||
});
|
||||
await expect(closing).rejects.toMatchObject({
|
||||
cleanup: {
|
||||
reason:
|
||||
"run:first, process-group:1234: first process group remains live; run:second, process-group:5678: second process group remains live",
|
||||
resources: [
|
||||
"run:first",
|
||||
"process-group:1234",
|
||||
"run:second",
|
||||
"process-group:5678",
|
||||
],
|
||||
},
|
||||
});
|
||||
await reader.close();
|
||||
expect(cleanupRetries).toBe(2);
|
||||
} finally {
|
||||
finishCommand(commandResult);
|
||||
await firstEntry.catch(() => undefined);
|
||||
await reader.close().catch(() => undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -633,6 +639,7 @@ describe("Mach-O slice stream ownership", () => {
|
||||
const release = createDeferred<void>();
|
||||
const nativeRead = source.handle.read.bind(source.handle);
|
||||
let reads: { mockRestore(): void } | undefined;
|
||||
let consumed: Promise<Buffer> | undefined;
|
||||
try {
|
||||
const [entry] = await sliceEntries(reader);
|
||||
reads = vi.spyOn(source.handle, "read").mockImplementation((async (
|
||||
@@ -643,8 +650,9 @@ describe("Mach-O slice stream ownership", () => {
|
||||
return Reflect.apply(nativeRead, source.handle, args);
|
||||
}) as typeof source.handle.read);
|
||||
const stream = await reader.open(entry!);
|
||||
stream.read(0);
|
||||
await reached.promise;
|
||||
consumed = buffer(stream);
|
||||
void consumed.catch(() => undefined);
|
||||
await awaitAcknowledgement(reached.promise, consumed, "slice read");
|
||||
let closed = false;
|
||||
const closing = reader.close().then(() => {
|
||||
closed = true;
|
||||
@@ -656,9 +664,13 @@ describe("Mach-O slice stream ownership", () => {
|
||||
expect(stream.closed).toBe(true);
|
||||
} finally {
|
||||
release.resolve();
|
||||
await reader.close();
|
||||
reads?.mockRestore();
|
||||
await source.handle.close();
|
||||
try {
|
||||
await reader.close();
|
||||
} finally {
|
||||
await consumed?.catch(() => undefined);
|
||||
reads?.mockRestore();
|
||||
await source.handle.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -729,7 +741,7 @@ it("joins Mach-O structural enumeration reads admitted before close", async () =
|
||||
const traversal = reader.entries()[Symbol.asyncIterator]();
|
||||
const next = traversal.next();
|
||||
try {
|
||||
await reached.promise;
|
||||
await awaitAcknowledgement(reached.promise, next, "slice enumeration read");
|
||||
let closed = false;
|
||||
const closing = reader.close().then(() => {
|
||||
closed = true;
|
||||
|
||||
@@ -7,8 +7,11 @@ import { ArtifactFileSource } from "./ArtifactFileSource.js";
|
||||
import { assertArtifactReadActive } from "./ArtifactFileOperation.js";
|
||||
import { NtfsMftArtifactReader } from "./NtfsMftArtifactReader.js";
|
||||
|
||||
import { MFT_DEFAULT_RECORD_BYTES } from "../domain/windows/ntfsMft.js";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
// The constructed FILE records intentionally use the 1024-byte default.
|
||||
const MFT_DEFAULT_RECORD_BYTES = 1024;
|
||||
import { linkedMftFixture } from "../../tests/fixtures/ntfsMftExtension.js";
|
||||
import { scanCanonicalArtifactInventory } from "../../tests/fixtures/artifactInventory.js";
|
||||
|
||||
@@ -212,7 +215,7 @@ it("retries a shared preparation cancelled by another traversal", async () => {
|
||||
const cancelledResult = expect(cancelled).rejects.toMatchObject({
|
||||
reason: "cancelled",
|
||||
});
|
||||
await started;
|
||||
await awaitAcknowledgement(started, cancelled, "NTFS traversal read");
|
||||
const other = reader.entries()[Symbol.asyncIterator]().next();
|
||||
controller.abort("cancelled first traversal");
|
||||
await cancelledResult;
|
||||
@@ -319,7 +322,7 @@ it("does not finish a traversal when close begins during final source verificati
|
||||
const rejected = expect(traversal).rejects.toMatchObject({
|
||||
reason: "unavailable",
|
||||
});
|
||||
await started;
|
||||
await awaitAcknowledgement(started, traversal, "NTFS close read");
|
||||
const closing = reader.close();
|
||||
resume();
|
||||
await rejected;
|
||||
|
||||
@@ -201,6 +201,8 @@ vi.mock("node:fs/promises", async () => {
|
||||
const PLANTED_SOURCE = "release-exposure-planted-secret-7c2e9a";
|
||||
const PLANTED_KEY = "AKIAPLANT9EXPOSUREXX";
|
||||
const ignoreText = (): void => undefined;
|
||||
// Keep the marker separate so Vite does not treat fixture text as this file's source map.
|
||||
const INLINE_MAP_PREFIX = "# sourceMappingURL=data:application/json;base64,";
|
||||
|
||||
describe("ReleaseExposureReader review regressions", () => {
|
||||
it("keeps inline CSS source-map coverage unknown in files and packages", async () => {
|
||||
@@ -2067,7 +2069,7 @@ const plistObject = (type: number, length: number, body: Buffer): Buffer =>
|
||||
|
||||
describe("ReleaseExposureReader text encodings", () => {
|
||||
const marker = "-----BEGIN PRIVATE KEY-----\n";
|
||||
const inlineMap = `//# sourceMappingURL=data:application/json;base64,${Buffer.from(
|
||||
const inlineMap = `//${INLINE_MAP_PREFIX}${Buffer.from(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["src/app.ts"],
|
||||
@@ -4328,7 +4330,7 @@ describe("ReleaseExposureReader script roles over a .map name", () => {
|
||||
});
|
||||
|
||||
describe("ReleaseExposureReader HTML script references", () => {
|
||||
const inlineMap = `//# sourceMappingURL=data:application/json;base64,${Buffer.from(
|
||||
const inlineMap = `//${INLINE_MAP_PREFIX}${Buffer.from(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["src/app.ts"],
|
||||
@@ -4620,7 +4622,7 @@ describe("ReleaseExposureReader cumulative text allowance", () => {
|
||||
|
||||
describe("ReleaseExposureReader unfollowed map links", () => {
|
||||
const SCRIPT = "const value = 1;\n//# sourceMappingURL=app.js.map\n";
|
||||
const EMBEDDED_PAYLOAD = `const value = 1;\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(
|
||||
const EMBEDDED_PAYLOAD = `const value = 1;\n//${INLINE_MAP_PREFIX}${Buffer.from(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["a.ts"],
|
||||
|
||||
@@ -5,6 +5,44 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import { bindArtifactFileCleanup } from "./ArtifactFileOperation.js";
|
||||
import { completedStableArtifactObservation } from "./StableArtifactObservation.js";
|
||||
import { isSelectedRegularFileError } from "../filesystem/RegularFile.js";
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisInputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
|
||||
/** Project filesystem errors that identify a refused or unreadable selected file. */
|
||||
export const projectSelectedArtifactFileFailure = (
|
||||
cause: unknown,
|
||||
operation: string,
|
||||
path: string,
|
||||
description: string,
|
||||
field = "path",
|
||||
): AnalysisError | undefined => {
|
||||
if (!(cause instanceof Error) || !("code" in cause)) return undefined;
|
||||
if (cause.code === "EACCES" || cause.code === "EPERM")
|
||||
return new AnalysisAccessDeniedError(operation, path, cause.code, {
|
||||
cause,
|
||||
});
|
||||
if (isSelectedRegularFileError(cause))
|
||||
return new AnalysisInputError(operation, { cause }, [
|
||||
{
|
||||
path: [field],
|
||||
reason: "invalid_value",
|
||||
message: `Selected ${description} could not be read (${String(cause.code)}): ${path}.`,
|
||||
},
|
||||
]);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Explain the selected image read budget and its fixed upper bound. */
|
||||
export const imageFileSizeRemediation = (
|
||||
selectedBytes: number,
|
||||
maximumBytes: number,
|
||||
): string =>
|
||||
selectedBytes >= maximumBytes
|
||||
? `The image exceeds the ${String(maximumBytes)}-byte input ceiling this operation accepts. This release has no runtime override. Select a smaller artifact.`
|
||||
: `Raise max_file_bytes above ${String(selectedBytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`;
|
||||
|
||||
/** Name an observed regular-file retry that admission will validate again. */
|
||||
export const regularFileSymlinkRecovery = (resolvedPath: string): string =>
|
||||
|
||||
@@ -103,9 +103,12 @@ it("preserves cancellation during payload CRC admission", async () => {
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await reader.close();
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
try {
|
||||
await reader.close();
|
||||
} finally {
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -149,9 +152,12 @@ it("admits a multi-image count before scanning the remaining payload", async ()
|
||||
results.reduce((sum, result) => sum + result.bytesRead, 0),
|
||||
).toBeLessThanOrEqual(UIMAGE_HEADER_SIZE + UIMAGE_MULTI_TABLE_WINDOW);
|
||||
} finally {
|
||||
await reader.close();
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
try {
|
||||
await reader.close();
|
||||
} finally {
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { setImmediate } from "node:timers/promises";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { buildUnityFs } from "../../tests/fixtures/unityfs.js";
|
||||
import { openRegularFile } from "../filesystem/RegularFile.js";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
@@ -185,9 +186,12 @@ describe("UnityFS allocation and malformed-input boundaries", () => {
|
||||
});
|
||||
expect(reads).toHaveBeenCalledTimes(1); // bounded header only
|
||||
} finally {
|
||||
await reader.close();
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
try {
|
||||
await reader.close();
|
||||
} finally {
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -223,6 +227,19 @@ describe("UnityFS allocation and malformed-input boundaries", () => {
|
||||
size,
|
||||
data: index === 0 ? Buffer.alloc(size, 0x61) : Buffer.alloc(0),
|
||||
}));
|
||||
const control = new UnityFsArtifactReader(
|
||||
await fixture(
|
||||
buildUnityFs(members.slice(0, 256), {
|
||||
blockSize: size,
|
||||
encodeBlock: repeatedBlock,
|
||||
}),
|
||||
),
|
||||
);
|
||||
try {
|
||||
expect(await entries(control)).toHaveLength(256);
|
||||
} finally {
|
||||
await control.close();
|
||||
}
|
||||
const reader = new UnityFsArtifactReader(
|
||||
await fixture(
|
||||
buildUnityFs(members, {
|
||||
@@ -232,7 +249,9 @@ describe("UnityFS allocation and malformed-input boundaries", () => {
|
||||
),
|
||||
);
|
||||
try {
|
||||
await expect(entries(reader)).rejects.toMatchObject({ reason: "limit" });
|
||||
await expect(entries(reader)).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
});
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
@@ -354,7 +373,7 @@ describe("UnityFS stream ownership and immutable cache", () => {
|
||||
let consumed: Promise<Buffer> | undefined;
|
||||
try {
|
||||
consumed = buffer(await reader.open(entry!, controller.signal));
|
||||
await reached.promise;
|
||||
await awaitAcknowledgement(reached.promise, consumed, "member read");
|
||||
controller.abort(new Error("caller cancelled"));
|
||||
release.resolve();
|
||||
await expect(consumed).rejects.toBe(failure);
|
||||
@@ -405,10 +424,12 @@ describe("UnityFS stream ownership and immutable cache", () => {
|
||||
await release.promise;
|
||||
return Reflect.apply(nativeRead, admitted.handle, args);
|
||||
}) as typeof admitted.handle.read);
|
||||
let consumed: Promise<Buffer> | undefined;
|
||||
try {
|
||||
const stream = await reader.open(entry!);
|
||||
stream.read(0);
|
||||
await reached.promise;
|
||||
consumed = buffer(stream);
|
||||
void consumed.catch(() => undefined);
|
||||
await awaitAcknowledgement(reached.promise, consumed, "member read");
|
||||
let closed = false;
|
||||
const closing = reader.close().then(() => {
|
||||
closed = true;
|
||||
@@ -420,9 +441,13 @@ describe("UnityFS stream ownership and immutable cache", () => {
|
||||
expect(stream.closed).toBe(true);
|
||||
} finally {
|
||||
release.resolve();
|
||||
await reader.close();
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
try {
|
||||
await reader.close();
|
||||
} finally {
|
||||
await consumed?.catch(() => undefined);
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -454,9 +479,12 @@ describe("UnityFS stream ownership and immutable cache", () => {
|
||||
buffer(await reader.open(entry!, controller.signal)),
|
||||
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
|
||||
} finally {
|
||||
await reader.close();
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
try {
|
||||
await reader.close();
|
||||
} finally {
|
||||
reads.mockRestore();
|
||||
await admitted.handle.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -474,12 +502,19 @@ describe("UnityFS stream capacity and cache ownership", () => {
|
||||
await fixture(buildUnityFs(members, { blockSize: 1 })),
|
||||
);
|
||||
try {
|
||||
let completed = 0;
|
||||
await expect(
|
||||
(async () => {
|
||||
for (const entry of await entries(reader))
|
||||
for (const entry of await entries(reader)) {
|
||||
await buffer(await reader.open(entry));
|
||||
completed += 1;
|
||||
}
|
||||
})(),
|
||||
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
});
|
||||
expect(completed).toBe(128);
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
|
||||
@@ -156,7 +156,10 @@ describe("wxapkg allocation and malformed-input boundaries", () => {
|
||||
await fixture(buildWxapkg(members, { aliasOffsets: true })),
|
||||
);
|
||||
try {
|
||||
await expect(entries(reader)).rejects.toMatchObject({ reason: "limit" });
|
||||
await expect(entries(reader)).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
message: expect.stringContaining("wxapkg members declare"),
|
||||
});
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
@@ -418,29 +421,25 @@ describe("wxapkg stream capacity and freshness", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds repeated member I/O operations, including empty members", async () => {
|
||||
it.each([
|
||||
{ path: "x", data: "b" },
|
||||
{ path: "empty.wxss", data: "" },
|
||||
])("bounds repeated I/O operations for $path", async (member) => {
|
||||
const reader = new WxapkgArtifactReader(
|
||||
await fixture(
|
||||
buildWxapkg([
|
||||
{ path: "x", data: "b" },
|
||||
{ path: "empty.wxss", data: "" },
|
||||
]),
|
||||
),
|
||||
await fixture(buildWxapkg([member])),
|
||||
);
|
||||
try {
|
||||
const [entry, emptyEntry] = await entries(reader);
|
||||
const [entry] = await entries(reader);
|
||||
await expect(
|
||||
(async () => {
|
||||
for (let index = 0; index < 5000; index++)
|
||||
await buffer(await reader.open(entry!));
|
||||
})(),
|
||||
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
await expect(
|
||||
(async () => {
|
||||
for (let index = 0; index < 5000; index++)
|
||||
await buffer(await reader.open(emptyEntry!));
|
||||
})(),
|
||||
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
).rejects.toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
message: expect.stringContaining("wxapkg directory-and-chunk I/O work"),
|
||||
});
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "../../tests/fixtures/zipZstandard.js";
|
||||
import { ArtifactReaderFailure, type ArtifactEntry } from "./ArtifactReader.js";
|
||||
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { once } from "node:events";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { constants, zstdCompressSync } from "node:zlib";
|
||||
@@ -836,7 +837,7 @@ it("joins an admitted ZIP enumeration before reader close completes", async () =
|
||||
const traversal = reader.entries()[Symbol.asyncIterator]();
|
||||
const next = traversal.next();
|
||||
try {
|
||||
await reached.promise;
|
||||
await awaitAcknowledgement(reached.promise, next, "ZIP enumeration read");
|
||||
let closed = false;
|
||||
const closing = reader.close().then(() => {
|
||||
closed = true;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { AnalysisAccessDeniedError } from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import { translateArtifactFailure } from "../ArtifactOperationFailure.js";
|
||||
import { DirectoryArtifactEntryAccessDenied } from "../DirectoryArtifactReader.js";
|
||||
import { inspectBundleKeyedArchive } from "./KeyedArchiveReader.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
versionMinCommand,
|
||||
} from "./MachoImage.fixture.js";
|
||||
import {
|
||||
MAX_LAZY_INFO_DEPENDENCIES,
|
||||
MAX_LAZY_INFO_SYMBOL_REFERENCES,
|
||||
MAX_LOAD_COMMAND_BYTES,
|
||||
readMachoImage,
|
||||
} from "./MachoLoadCommandReader.js";
|
||||
@@ -35,6 +33,10 @@ import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { javaClassFile } from "../../../tests/fixtures/javaClassFile.js";
|
||||
|
||||
// Independently exercise the dependency and reference budgets.
|
||||
const MAX_LAZY_INFO_DEPENDENCIES = 1024;
|
||||
const MAX_LAZY_INFO_SYMBOL_REFERENCES = 64 * 1024;
|
||||
|
||||
const read = (bytes: Uint8Array) =>
|
||||
readMachoImage(readerOf(bytes), bytes.length);
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@ import type {
|
||||
MachoImageFacts,
|
||||
MachoSlice,
|
||||
} from "../../domain/apple/dylibResolution.js";
|
||||
import {
|
||||
machoArchitectureName,
|
||||
machoCpuSubtypeBase,
|
||||
} from "../../domain/apple/machoCpuIdentity.js";
|
||||
import { machoArchitectureName } from "../../domain/apple/machoCpuIdentity.js";
|
||||
import {
|
||||
legacyMachoPlatform,
|
||||
type MachoVersionMinPlatform,
|
||||
@@ -52,13 +49,13 @@ export const MAX_LOAD_COMMAND_BYTES = 16 * 1024 * 1024;
|
||||
* byte cap alone would retain about a million dependency results. Stop before
|
||||
* those results are kept or traced.
|
||||
*/
|
||||
export const MAX_LAZY_INFO_DEPENDENCIES = 1024;
|
||||
const MAX_LAZY_INFO_DEPENDENCIES = 1024;
|
||||
/**
|
||||
* Symbol names are validated and then discarded. Distinct one-byte strings
|
||||
* stay under the 16 MiB byte budgets while retaining one cache entry per
|
||||
* offset, so stop before that map and those decodes grow.
|
||||
*/
|
||||
export const MAX_LAZY_INFO_SYMBOL_REFERENCES = 64 * 1024;
|
||||
const MAX_LAZY_INFO_SYMBOL_REFERENCES = 64 * 1024;
|
||||
|
||||
const LC_SEGMENT = 0x1;
|
||||
const LC_SEGMENT_64 = 0x19;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
||||
import { buffer } from "node:stream/consumers";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import * as artifactPaths from "../ArtifactPaths.js";
|
||||
import { BunStandaloneArtifactReader } from "../BunStandaloneArtifactReader.js";
|
||||
import { inventoryArtifact } from "../inventory/ArtifactInventory.js";
|
||||
@@ -284,7 +285,7 @@ it("keeps another caller's in-flight inventory independent of cancelled preparat
|
||||
},
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
await started;
|
||||
await awaitAcknowledgement(started, first, "Bun artifact read");
|
||||
const second = collect(reader.entries());
|
||||
controller.abort(reason);
|
||||
resume();
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
MAX_DENO_METADATA_JSON_BYTES,
|
||||
MAX_DENO_TABLE_COUNT,
|
||||
parseDenoSection,
|
||||
} from "./DenoSectionLayout.js";
|
||||
import { parseDenoSection } from "./DenoSectionLayout.js";
|
||||
import {
|
||||
buildDenoSection,
|
||||
defaultDenoMetadata,
|
||||
@@ -130,7 +126,7 @@ it("treats a missing opening marker as an unsupported layout", async () => {
|
||||
});
|
||||
|
||||
it("refuses metadata JSON above the named allowance before reading it as JSON", async () => {
|
||||
const claimed = MAX_DENO_METADATA_JSON_BYTES + 1;
|
||||
const claimed = 8 * 1024 * 1024 + 1;
|
||||
const bytes = Buffer.alloc(DENO_FIXTURE_SECTION_MAGIC.length + 8 + claimed);
|
||||
DENO_FIXTURE_SECTION_MAGIC.copy(bytes, 0);
|
||||
bytes.writeBigUInt64LE(BigInt(claimed), DENO_FIXTURE_SECTION_MAGIC.length);
|
||||
@@ -145,7 +141,7 @@ it("refuses a specifier table above the named record allowance", async () => {
|
||||
parseDenoSectionBytes(
|
||||
buildDenoSection({
|
||||
specifiers: [],
|
||||
specifierCount: MAX_DENO_TABLE_COUNT + 1,
|
||||
specifierCount: 100_001,
|
||||
}),
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
|
||||
@@ -9,9 +9,9 @@ import { walkDenoVirtualFs, type DenoVfsWalk } from "./DenoVirtualFs.js";
|
||||
/** Opening and closing marker of a Deno compile data section. */
|
||||
export const DENO_SECTION_MAGIC = Buffer.from("d3n0l4nd", "latin1");
|
||||
|
||||
export const MAX_DENO_METADATA_JSON_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_DENO_METADATA_JSON_BYTES = 8 * 1024 * 1024;
|
||||
export const MAX_DENO_VFS_HEADER_JSON_BYTES = 32 * 1024 * 1024;
|
||||
export const MAX_DENO_TABLE_COUNT = 100_000;
|
||||
const MAX_DENO_TABLE_COUNT = 100_000;
|
||||
export const MAX_DENO_SPECIFIER_TEXT_BYTES = 8 * 1024 * 1024;
|
||||
export const MAX_DENO_REMOTE_BODY_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { open, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { elfNote, elfNoteFixture } from "../../native/elf/ElfNotes.fixture.js";
|
||||
import {
|
||||
@@ -19,10 +20,7 @@ import { artifactFormatDescriptor } from "../artifactFormats.js";
|
||||
import { ArtifactPathRegistry } from "../ArtifactPaths.js";
|
||||
import { DenoStandaloneArtifactReader } from "../DenoStandaloneArtifactReader.js";
|
||||
import { cArchiveExecutable } from "../pyinstaller/CArchive.fixture.js";
|
||||
import {
|
||||
locateDenoStandalone,
|
||||
MAX_DENO_INTEL_TRAILER_SENTINELS,
|
||||
} from "./DenoStandaloneLocation.js";
|
||||
import { locateDenoStandalone } from "./DenoStandaloneLocation.js";
|
||||
import {
|
||||
buildDenoSection,
|
||||
DENO_FIXTURE_SECTION_MAGIC,
|
||||
@@ -34,9 +32,11 @@ import {
|
||||
denoUniversal,
|
||||
machOWithSegment,
|
||||
} from "./DenoStandalone.fixture.js";
|
||||
import { MAX_DENO_PATH_COMPONENT_BYTES } from "./DenoVirtualFs.js";
|
||||
|
||||
import { resolveArtifactReaderLimits } from "../../domain/artifactReaderLimits.js";
|
||||
|
||||
const MAX_DENO_PATH_COMPONENT_BYTES = 255;
|
||||
|
||||
it("keeps a confirmed Deno inventory when another pack recognizer is limited", async () => {
|
||||
const original = denoElf();
|
||||
// Add a valid empty section table without changing the existing PT_NOTE.
|
||||
@@ -1071,7 +1071,7 @@ it("rejects packed Intel sentinels without rereading a chunk per sentinel", asyn
|
||||
it("leaves an x64 Mach-O absent when sentinels pass the allowance", async () => {
|
||||
const packed = Buffer.concat([
|
||||
denoMachO([], { architecture: "x64" }),
|
||||
rejectedIntelSentinels(MAX_DENO_INTEL_TRAILER_SENTINELS + 1),
|
||||
rejectedIntelSentinels(65),
|
||||
]);
|
||||
let readBytes = 0;
|
||||
const probe = await locateDenoStandalone(
|
||||
@@ -1307,7 +1307,7 @@ it("accepts side-by-side Intel trailers with identical bytes", async () => {
|
||||
it("refuses a confirmed Intel trailer when the allowance ends the outer search", async () => {
|
||||
const bytes = Buffer.concat([
|
||||
denoMachO([], { architecture: "x64" }),
|
||||
rejectedIntelSentinels(MAX_DENO_INTEL_TRAILER_SENTINELS),
|
||||
rejectedIntelSentinels(64),
|
||||
intelTrailerRecord(buildDenoSection()),
|
||||
]);
|
||||
await expect(
|
||||
@@ -1876,7 +1876,7 @@ it.each(["retry", "independent caller", "physical failure"] as const)(
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
pending.push(failure);
|
||||
await entered;
|
||||
await awaitAcknowledgement(entered, first, "Deno artifact read");
|
||||
const concurrent =
|
||||
scenario === "independent caller"
|
||||
? collect(reader.entries())
|
||||
|
||||
@@ -64,7 +64,7 @@ const ELF_NOTE_TYPE = 0x53554901;
|
||||
const PE_HEADER_PROBE_BYTES = 64 * 1024;
|
||||
const INTEL_CHUNK_BYTES = 1024 * 1024;
|
||||
/** Trailer candidates examined before the search stops. */
|
||||
export const MAX_DENO_INTEL_TRAILER_SENTINELS = 64;
|
||||
const MAX_DENO_INTEL_TRAILER_SENTINELS = 64;
|
||||
const INTEL_SENTINEL = Buffer.concat([
|
||||
Buffer.from("<~sui-data~>", "latin1"),
|
||||
Buffer.from([0xef, 0xbe, 0xad, 0xde]),
|
||||
|
||||
@@ -2,15 +2,17 @@ import { expect, it } from "vitest";
|
||||
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import {
|
||||
MAX_DENO_LOGICAL_PATH_BYTES,
|
||||
MAX_DENO_LOGICAL_PATH_PARTS,
|
||||
MAX_DENO_PATH_COMPONENT_BYTES,
|
||||
MAX_DENO_VFS_DEPTH,
|
||||
MAX_DENO_VFS_ENTRIES,
|
||||
MAX_DENO_VFS_NAME_UNITS,
|
||||
walkDenoVirtualFs,
|
||||
} from "./DenoVirtualFs.js";
|
||||
|
||||
// Independent format and admission examples.
|
||||
const MAX_DENO_VFS_ENTRIES = 100_000;
|
||||
const MAX_DENO_VFS_NAME_UNITS = 4_096;
|
||||
const MAX_DENO_VFS_DEPTH = 512;
|
||||
const MAX_DENO_PATH_COMPONENT_BYTES = 255;
|
||||
const MAX_DENO_LOGICAL_PATH_BYTES = 512;
|
||||
|
||||
const file = (
|
||||
name: string,
|
||||
offset: number,
|
||||
|
||||
@@ -4,32 +4,32 @@ import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import { normalizeArtifactPath } from "../ArtifactPaths.js";
|
||||
|
||||
/** Directory, file, and symlink records admitted from one virtual filesystem. */
|
||||
export const MAX_DENO_VFS_ENTRIES = 100_000;
|
||||
const MAX_DENO_VFS_ENTRIES = 100_000;
|
||||
/** Logical path parts admitted before an indexed fallback. */
|
||||
export const MAX_DENO_LOGICAL_PATH_PARTS = 256;
|
||||
/**
|
||||
* UTF-16 code units retained for one virtual name. Longer names are indexed
|
||||
* and the original spelling is not copied into members.
|
||||
*/
|
||||
export const MAX_DENO_VFS_NAME_UNITS = 4_096;
|
||||
const MAX_DENO_VFS_NAME_UNITS = 4_096;
|
||||
/**
|
||||
* Nested `walkEntries` calls. This is the recursion budget, separate from
|
||||
* the logical-path part fallback.
|
||||
*/
|
||||
export const MAX_DENO_VFS_DEPTH = 512;
|
||||
const MAX_DENO_VFS_DEPTH = 512;
|
||||
/**
|
||||
* UTF-16 code units of the source path built for each entry, directories
|
||||
* included, plus retained symlink parts. Each path is rebuilt and normalized
|
||||
* once, so this also bounds the synchronous walk.
|
||||
*/
|
||||
export const MAX_DENO_VFS_RETAINED_UNITS = 32 * 1024 * 1024;
|
||||
const MAX_DENO_VFS_RETAINED_UNITS = 32 * 1024 * 1024;
|
||||
/** POSIX NAME_MAX. A longer component makes extraction fail with ENAMETOOLONG. */
|
||||
export const MAX_DENO_PATH_COMPONENT_BYTES = 255;
|
||||
const MAX_DENO_PATH_COMPONENT_BYTES = 255;
|
||||
/**
|
||||
* Natural logical path bytes. Half of the 1,024-byte Darwin PATH_MAX, leaving
|
||||
* the rest for the output root, so a long path does not abort extraction.
|
||||
*/
|
||||
export const MAX_DENO_LOGICAL_PATH_BYTES = 512;
|
||||
const MAX_DENO_LOGICAL_PATH_BYTES = 512;
|
||||
/** Logical path suffixes of the regions stored beside an original file. */
|
||||
export const DENO_CHILD_SUFFIXES = {
|
||||
transpiled: ".deno-transpiled",
|
||||
|
||||
@@ -106,10 +106,13 @@ it.each(cases)(
|
||||
).toHaveLength(2);
|
||||
expect(scope.hasPending).toBe(false);
|
||||
} finally {
|
||||
await scope.close();
|
||||
observation.mockRestore();
|
||||
close.mockRestore();
|
||||
release.mockRestore();
|
||||
try {
|
||||
await scope.close();
|
||||
} finally {
|
||||
observation.mockRestore();
|
||||
close.mockRestore();
|
||||
release.mockRestore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -146,8 +149,11 @@ it.each(cases)(
|
||||
await expect(stat(output)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(scope.hasPending).toBe(false);
|
||||
} finally {
|
||||
await scope.close();
|
||||
close.mockRestore();
|
||||
try {
|
||||
await scope.close();
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -295,8 +301,11 @@ it.each(cases)(
|
||||
expect(closes).toBe(2);
|
||||
expect(scope.hasPending).toBe(false);
|
||||
} finally {
|
||||
await scope.close();
|
||||
close.mockRestore();
|
||||
try {
|
||||
await scope.close();
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -378,8 +387,11 @@ it.each(
|
||||
}
|
||||
expect(scope.hasPending).toBe(false);
|
||||
} finally {
|
||||
await scope.close();
|
||||
close.mockRestore();
|
||||
try {
|
||||
await scope.close();
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -12,13 +12,7 @@ import {
|
||||
fitFixture,
|
||||
type FitFixtureImage,
|
||||
} from "./FitImage.fixture.js";
|
||||
import {
|
||||
FIT_MAX_CONFIGURATION_REFERENCES,
|
||||
FIT_MAX_CONFIGURATIONS,
|
||||
FIT_MAX_HASH_NODES_PER_IMAGE,
|
||||
FIT_MAX_IMAGES,
|
||||
readFitStructure,
|
||||
} from "./FitStructure.js";
|
||||
import { readFitStructure } from "./FitStructure.js";
|
||||
|
||||
const read = (file: Buffer) =>
|
||||
readFitStructure(
|
||||
@@ -487,75 +481,74 @@ describe("FIT configurations", () => {
|
||||
|
||||
describe("FIT fixed limits", () => {
|
||||
it("fails more images than the fixed allowance", () => {
|
||||
expect(
|
||||
failureOf(
|
||||
fitFixture({
|
||||
images: Array.from({ length: FIT_MAX_IMAGES + 1 }, (_, index) => ({
|
||||
name: `image-${String(index)}`,
|
||||
data: Buffer.from([index & 0xff]),
|
||||
})),
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
const make = (count: number) =>
|
||||
fitFixture({
|
||||
images: Array.from({ length: count }, (_, index) => ({
|
||||
name: `image-${String(index)}`,
|
||||
data: Buffer.from([index & 0xff]),
|
||||
})),
|
||||
});
|
||||
expect(read(make(1024)).images).toHaveLength(1024);
|
||||
expect(failureOf(make(1025))).toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
});
|
||||
});
|
||||
|
||||
it("fails more hash nodes than the fixed allowance", () => {
|
||||
expect(
|
||||
failureOf(
|
||||
fitFixture({
|
||||
images: [
|
||||
{
|
||||
...kernel(),
|
||||
hashes: Array.from(
|
||||
{ length: FIT_MAX_HASH_NODES_PER_IMAGE + 1 },
|
||||
() => "crc32",
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
const make = (count: number) =>
|
||||
fitFixture({
|
||||
images: [
|
||||
{
|
||||
...kernel(),
|
||||
hashes: Array.from({ length: count }, () => "crc32"),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(read(make(8)).images[0]?.hashes).toHaveLength(8);
|
||||
expect(failureOf(make(9))).toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
});
|
||||
});
|
||||
|
||||
it("fails more configurations than the fixed allowance", () => {
|
||||
expect(
|
||||
failureOf(
|
||||
fitFixture({
|
||||
images: [kernel()],
|
||||
configurations: {
|
||||
name: "configurations",
|
||||
children: Array.from(
|
||||
{ length: FIT_MAX_CONFIGURATIONS + 1 },
|
||||
(_, index) => ({ name: `conf-${String(index)}` }),
|
||||
),
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
const make = (count: number) =>
|
||||
fitFixture({
|
||||
images: [kernel()],
|
||||
configurations: {
|
||||
name: "configurations",
|
||||
children: Array.from({ length: count }, (_, index) => ({
|
||||
name: `conf-${String(index)}`,
|
||||
})),
|
||||
},
|
||||
});
|
||||
expect(read(make(1024)).metadata.configurations).toHaveLength(1024);
|
||||
expect(failureOf(make(1025))).toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds configuration references before decoding them", () => {
|
||||
expect(
|
||||
failureOf(
|
||||
fitFixture({
|
||||
images: [kernel()],
|
||||
configurations: {
|
||||
name: "configurations",
|
||||
children: [
|
||||
{
|
||||
name: "conf-1",
|
||||
properties: {
|
||||
loadables: Buffer.alloc(FIT_MAX_CONFIGURATION_REFERENCES + 1),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
const make = (count: number) =>
|
||||
fitFixture({
|
||||
images: [kernel()],
|
||||
configurations: {
|
||||
name: "configurations",
|
||||
children: [
|
||||
{ name: "conf-1", properties: { loadables: Buffer.alloc(count) } },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(read(make(16_384)).metadata.configurations).toHaveLength(1);
|
||||
expect(failureOf(make(16_385))).toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps a parser limit to a fixed allowance without a caller limit", () => {
|
||||
it("does not attach caller-adjustable admission to a fixed parser limit", () => {
|
||||
let nested: FixtureNode = { name: "leaf" };
|
||||
for (let depth = 0; depth < 64; depth += 1)
|
||||
nested = { name: `n${String(depth)}`, children: [nested] };
|
||||
@@ -565,9 +558,12 @@ describe("FIT fixed limits", () => {
|
||||
children: [{ name: "images" }, nested],
|
||||
}),
|
||||
);
|
||||
expect(failure).toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
// The parser's caller-limit recovery does not apply to fixed FIT reads.
|
||||
expect(String((failure as Error).message)).not.toContain("caller-selected");
|
||||
expect(failure).toMatchObject({
|
||||
reason: "limit",
|
||||
boundary: "capacity",
|
||||
readerLimit: undefined,
|
||||
admission: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails a structure without /images", () => {
|
||||
@@ -580,10 +576,7 @@ describe("FIT fixed limits", () => {
|
||||
|
||||
it("bounds dangling-reference diagnostics while preserving every declared reference", () => {
|
||||
const name = "c".repeat(4096);
|
||||
const references = Array.from(
|
||||
{ length: FIT_MAX_CONFIGURATION_REFERENCES },
|
||||
() => "missing",
|
||||
);
|
||||
const references = Array.from({ length: 16_384 }, () => "missing");
|
||||
const file = fitFixture({
|
||||
images: [{ name: "kernel", data: Buffer.from("K") }],
|
||||
configurations: {
|
||||
|
||||
@@ -18,11 +18,11 @@ import { BinaryFormatFailure } from "../../native/metadata/BinaryContainer.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
|
||||
// Fixed resource bounds; configurable ceilings are tracked in #6176.
|
||||
export const FIT_MAX_IMAGES = 1024;
|
||||
export const FIT_MAX_HASH_NODES_PER_IMAGE = 8;
|
||||
export const FIT_MAX_CONFIGURATIONS = 1024;
|
||||
const FIT_MAX_IMAGES = 1024;
|
||||
const FIT_MAX_HASH_NODES_PER_IMAGE = 8;
|
||||
const FIT_MAX_CONFIGURATIONS = 1024;
|
||||
/** Bounds configuration references and their limitations across the FIT. */
|
||||
export const FIT_MAX_CONFIGURATION_REFERENCES = 16_384;
|
||||
const FIT_MAX_CONFIGURATION_REFERENCES = 16_384;
|
||||
|
||||
type FitNode = Devicetree["nodes"][number];
|
||||
type FitProperty = FitNode["properties"][number];
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
nesContainerFormat,
|
||||
parseInesLayout,
|
||||
} from "../../domain/inesContainer.js";
|
||||
import type { GodotPckAdmission } from "../GodotPckLocation.js";
|
||||
import {
|
||||
resolveArtifactReaderLimits,
|
||||
type ArtifactReaderLimits,
|
||||
} from "../../domain/artifactReaderLimits.js";
|
||||
import type { PyInstallerAdmission } from "../pyinstaller/PyInstallerAdmission.js";
|
||||
import type {
|
||||
ArtifactReaderAdmission,
|
||||
ArtifactFormatAdmission,
|
||||
|
||||
@@ -81,9 +81,7 @@ export const buildNodeSeaBlob = (
|
||||
return Buffer.concat(parts);
|
||||
};
|
||||
|
||||
export const nodeSeaFuseBytes = (
|
||||
state: NodeSeaFuseFixture = "absent",
|
||||
): Buffer => {
|
||||
const nodeSeaFuseBytes = (state: NodeSeaFuseFixture = "absent"): Buffer => {
|
||||
if (state === "absent") return Buffer.alloc(0);
|
||||
const sentinel = (digit: string, hash = NODE_SEA_FUSE_HASH): Buffer =>
|
||||
Buffer.from(`\0NODE_SEA_FUSE_${hash}:${digit}\0`, "latin1");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { withPyInstallerReader } from "../../../tests/fixtures/pyinstallerReader.js";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js";
|
||||
import type { ArtifactEntry } from "../ArtifactReader.js";
|
||||
import { PyInstallerPyzStore } from "./PyInstallerPyzStore.js";
|
||||
@@ -114,7 +115,11 @@ it.each([false, true])(
|
||||
const opening = reader.open(selected, signal.signal);
|
||||
void opening.catch(() => undefined);
|
||||
try {
|
||||
await started;
|
||||
await awaitAcknowledgement(
|
||||
started,
|
||||
opening,
|
||||
"PyInstaller image admission",
|
||||
);
|
||||
signal.abort(cancelled);
|
||||
release();
|
||||
await expect(opening).rejects.toMatchObject({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { withPyInstallerReader } from "../../../tests/fixtures/pyinstallerReader.js";
|
||||
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
@@ -392,7 +393,11 @@ it.each([false, true])(
|
||||
void consumed.catch(() => undefined);
|
||||
let closing: Promise<void> | undefined;
|
||||
try {
|
||||
await entered.promise;
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
consumed,
|
||||
"PyInstaller operation",
|
||||
);
|
||||
controller.abort(caller);
|
||||
let closed = false;
|
||||
closing = reader.close().then(() => {
|
||||
@@ -616,7 +621,11 @@ it("joins a late private-file close and retries its failed owner after cancellat
|
||||
.next()
|
||||
.catch((cause: unknown) => cause);
|
||||
try {
|
||||
await entered.promise;
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
listing,
|
||||
"PyInstaller operation",
|
||||
);
|
||||
let settled = false;
|
||||
const closing = reader.close().then(() => {
|
||||
settled = true;
|
||||
@@ -690,7 +699,11 @@ for (const cancel of [false, true]) {
|
||||
(cause: unknown) => ({ ok: false as const, cause }),
|
||||
);
|
||||
try {
|
||||
await entered.promise;
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
reading,
|
||||
"PyInstaller operation",
|
||||
);
|
||||
expect(output.readableEnded).toBe(false);
|
||||
expect(owner?.handle.fd).toBeGreaterThanOrEqual(0);
|
||||
if (cancel) {
|
||||
@@ -825,7 +838,11 @@ it.each([false, true])(
|
||||
.catch((cause: unknown) => cause);
|
||||
let closing: Promise<void> | undefined;
|
||||
try {
|
||||
await entered.promise;
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
first,
|
||||
"PyInstaller operation",
|
||||
);
|
||||
await setImmediate();
|
||||
expect(handles.size).toBe(1);
|
||||
caller.abort(reason);
|
||||
@@ -928,7 +945,11 @@ it.each(
|
||||
);
|
||||
let second: Promise<ArtifactEntry[]> | undefined;
|
||||
try {
|
||||
await entered.promise;
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
first,
|
||||
"PyInstaller operation",
|
||||
);
|
||||
expect(reader.formatMetadata()).toBeUndefined();
|
||||
if (peer) {
|
||||
second = members(reader);
|
||||
@@ -1031,7 +1052,11 @@ it("keeps a physical parse failure primary despite simultaneous caller cancellat
|
||||
);
|
||||
let second: Promise<unknown> | undefined;
|
||||
try {
|
||||
await entered.promise;
|
||||
await awaitAcknowledgement(
|
||||
entered.promise,
|
||||
first,
|
||||
"PyInstaller operation",
|
||||
);
|
||||
second = members(reader).catch((cause: unknown) => cause);
|
||||
release.resolve();
|
||||
const original = await first;
|
||||
@@ -1042,8 +1067,7 @@ it("keeps a physical parse failure primary despite simultaneous caller cancellat
|
||||
expect(reader.formatMetadata()).toBeUndefined();
|
||||
} finally {
|
||||
release.resolve();
|
||||
await first;
|
||||
await second;
|
||||
await Promise.allSettled([first, ...(second ? [second] : [])]);
|
||||
reads.mockRestore();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { open, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { readPyzArchive, MAX_PYZ_TABLE_BYTES } from "./PyzArchive.js";
|
||||
import { readPyzArchive } from "./PyzArchive.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { PYTHON_39_MAGIC, pyzArchive } from "./CArchive.fixture.js";
|
||||
|
||||
@@ -82,7 +82,7 @@ it.each(["PYZ\0", "BAD\0"])(
|
||||
await writeFile(path, header);
|
||||
const handle = await open(path, "r+");
|
||||
try {
|
||||
const size = 12 + MAX_PYZ_TABLE_BYTES + 1;
|
||||
const size = 12 + 16 * 1024 * 1024 + 1;
|
||||
await handle.truncate(size);
|
||||
expect(await readPyzArchive(handle, 0, size)).toMatchObject({
|
||||
ok: false,
|
||||
|
||||
@@ -44,7 +44,7 @@ export type PyzArchive =
|
||||
};
|
||||
|
||||
/** Bound the transient marshal table allocation independently of image expansion. */
|
||||
export const MAX_PYZ_TABLE_BYTES = 16 * 1024 * 1024;
|
||||
const MAX_PYZ_TABLE_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
interface PyzHeaderFacts {
|
||||
readonly signatureMatches: boolean;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { jsonPointerTokens } from "../domain/jsonPointer.js";
|
||||
import { visit, type JSONPath } from "jsonc-parser";
|
||||
import { isExactJsonNumber } from "../domain/exactJsonNumber.js";
|
||||
import { safeParseJson } from "../domain/safeJson.js";
|
||||
@@ -22,14 +23,6 @@ export interface ResponseValue {
|
||||
readonly value: string | number;
|
||||
}
|
||||
|
||||
const pointerTokens = (pointer: string): readonly string[] =>
|
||||
pointer === ""
|
||||
? []
|
||||
: pointer
|
||||
.slice(1)
|
||||
.split("/")
|
||||
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
|
||||
|
||||
/** Keep unrelated fields usable while marking numbers that cannot be selected exactly. */
|
||||
export const parseScenarioValueJson = (text: string) =>
|
||||
safeParseJson(text, (_key, value, context) =>
|
||||
@@ -44,7 +37,7 @@ export const responsePointerValue = (
|
||||
pointer: string,
|
||||
): unknown => {
|
||||
let current = value;
|
||||
for (const key of pointerTokens(pointer)) {
|
||||
for (const key of jsonPointerTokens(pointer)) {
|
||||
if (Array.isArray(current)) {
|
||||
if (!/^(?:0|[1-9][0-9]*)(?![\s\S])/u.test(key)) return undefined;
|
||||
current = current[Number(key)];
|
||||
@@ -191,7 +184,7 @@ export const redactResponseSource = (
|
||||
): Result<Buffer | undefined, ResponseValueIssue> => {
|
||||
const markers = new Map(
|
||||
values.map(({ entry, value }) => [
|
||||
JSON.stringify(pointerTokens(entry.json_pointer)),
|
||||
JSON.stringify(jsonPointerTokens(entry.json_pointer)),
|
||||
redact(String(value)),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -2,11 +2,9 @@ import {
|
||||
cleanupOwnedProcessGroup,
|
||||
cleanupWindowsProcessTree,
|
||||
verifyNoTokenOwnedProcesses,
|
||||
type OwnedProcessGroup,
|
||||
type PersistentOwnedProcessGroup,
|
||||
type ProcessCleanupResult,
|
||||
type ProcessLineageObservation,
|
||||
type OwnershipSweepUnverifiedProcess,
|
||||
type ProcessOwnershipHost,
|
||||
} from "../process/ProcessOwnership.js";
|
||||
import { systemProcessOwnershipHost } from "../process/ProcessOwnershipObservation.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js";
|
||||
|
||||
@@ -233,10 +234,19 @@ it("carries the exact caller cancellation reason and completed prefix facts", as
|
||||
input,
|
||||
controller.signal,
|
||||
);
|
||||
await started.promise;
|
||||
controller.abort(exactReason);
|
||||
const result = await pending;
|
||||
analyzer.releaseReservation();
|
||||
let result: Awaited<typeof pending>;
|
||||
try {
|
||||
await awaitAcknowledgement(started.promise, pending, "script analysis");
|
||||
controller.abort(exactReason);
|
||||
result = await pending;
|
||||
} finally {
|
||||
controller.abort(exactReason);
|
||||
try {
|
||||
await pending;
|
||||
} finally {
|
||||
analyzer.releaseReservation();
|
||||
}
|
||||
}
|
||||
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, expect, it, vi } from "vitest";
|
||||
import type { ElectronApplication } from "playwright-core";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import {
|
||||
electronActiveObservationInputSchema,
|
||||
@@ -791,20 +792,33 @@ it.each(["acquisition", "identity"] as const)(
|
||||
{ signal: controller.signal },
|
||||
resources,
|
||||
);
|
||||
await started.promise;
|
||||
controller.abort(new Error("stop during selected window read"));
|
||||
await Promise.resolve();
|
||||
finish.resolve();
|
||||
const outcome = await running;
|
||||
expect(outcome.actions[0]).toMatchObject({
|
||||
status: "cancelled",
|
||||
target: null,
|
||||
});
|
||||
expect(page.locator).not.toHaveBeenCalled();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
expect(resources.hasPending).toBe(false);
|
||||
await resources.close();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
started.promise,
|
||||
running,
|
||||
"selected window read",
|
||||
);
|
||||
controller.abort(new Error("stop during selected window read"));
|
||||
await Promise.resolve();
|
||||
finish.resolve();
|
||||
const outcome = await running;
|
||||
expect(outcome.actions[0]).toMatchObject({
|
||||
status: "cancelled",
|
||||
target: null,
|
||||
});
|
||||
expect(page.locator).not.toHaveBeenCalled();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
expect(resources.hasPending).toBe(false);
|
||||
await resources.close();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
finish.resolve();
|
||||
try {
|
||||
await running;
|
||||
} finally {
|
||||
await resources.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterEach, expect, it, vi } from "vitest";
|
||||
import { mkdir, writeFile, rm, access, symlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import type {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
@@ -242,34 +243,42 @@ it.each([false, true])(
|
||||
(value) => ({ ok: true as const, value }),
|
||||
(cause: unknown) => ({ ok: false as const, cause }),
|
||||
);
|
||||
await started.promise;
|
||||
if (cancelled) controller.abort(reason);
|
||||
launched.resolve({
|
||||
const context = {
|
||||
close,
|
||||
browser: () => ({}) as Browser,
|
||||
pages: () => [],
|
||||
newPage,
|
||||
} as unknown as BrowserContext);
|
||||
const result = await opening;
|
||||
if (result.ok) {
|
||||
try {
|
||||
expect(cancelled).toBe(false);
|
||||
expect(newPage).toHaveBeenCalledOnce();
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
expect(result.value.profilePath).toBe(profile);
|
||||
} finally {
|
||||
await result.value.cleanup.close();
|
||||
} as unknown as BrowserContext;
|
||||
try {
|
||||
await awaitAcknowledgement(started.promise, opening, "browser launch");
|
||||
if (cancelled) controller.abort(reason);
|
||||
launched.resolve(context);
|
||||
const result = await opening;
|
||||
if (result.ok) {
|
||||
try {
|
||||
expect(cancelled).toBe(false);
|
||||
expect(newPage).toHaveBeenCalledOnce();
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
expect(result.value.profilePath).toBe(profile);
|
||||
} finally {
|
||||
await result.value.cleanup.close();
|
||||
}
|
||||
} else {
|
||||
expect(cancelled).toBe(true);
|
||||
expect(result.cause).toMatchObject({ reason: "cancelled" });
|
||||
if (!(result.cause instanceof AnalysisError))
|
||||
throw new Error("Expected typed cancellation");
|
||||
expect(result.cause.cause).toBe(reason);
|
||||
expect(newPage).not.toHaveBeenCalled();
|
||||
}
|
||||
} else {
|
||||
expect(cancelled).toBe(true);
|
||||
expect(result.cause).toMatchObject({ reason: "cancelled" });
|
||||
if (!(result.cause instanceof AnalysisError))
|
||||
throw new Error("Expected typed cancellation");
|
||||
expect(result.cause.cause).toBe(reason);
|
||||
expect(newPage).not.toHaveBeenCalled();
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
await expect(access(profile)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
controller.abort(reason);
|
||||
launched.resolve(context);
|
||||
const outcome = await opening;
|
||||
if (outcome.ok) await outcome.value.cleanup.close();
|
||||
}
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
await expect(access(profile)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -428,16 +437,27 @@ it("does not configure a late attached connection after cancellation", async ()
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
await connectionStarted.promise;
|
||||
controller.abort();
|
||||
connection.resolve(connectedBrowser);
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
connectionStarted.promise,
|
||||
opening,
|
||||
"CDP connection",
|
||||
);
|
||||
controller.abort();
|
||||
connection.resolve(connectedBrowser);
|
||||
|
||||
await expect(opening).rejects.toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "cancelled",
|
||||
});
|
||||
expect(connectedBrowser.contexts).not.toHaveBeenCalled();
|
||||
expect(connectedBrowser.close).toHaveBeenCalledOnce();
|
||||
await expect(opening).rejects.toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "cancelled",
|
||||
});
|
||||
expect(connectedBrowser.contexts).not.toHaveBeenCalled();
|
||||
expect(connectedBrowser.close).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
controller.abort();
|
||||
connection.resolve(connectedBrowser);
|
||||
const opened = await opening.catch(() => undefined);
|
||||
await opened?.cleanup.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("closes an attached connection when cancellation starts target discovery", async () => {
|
||||
@@ -651,10 +671,19 @@ it("checks a late stop after attached restoration and before transport disconnec
|
||||
},
|
||||
});
|
||||
|
||||
await restorationStarted.promise;
|
||||
revision += 1;
|
||||
finishRestoration.resolve(undefined);
|
||||
await release;
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
restorationStarted.promise,
|
||||
release,
|
||||
"browser restoration",
|
||||
);
|
||||
revision += 1;
|
||||
finishRestoration.resolve(undefined);
|
||||
await release;
|
||||
} finally {
|
||||
finishRestoration.resolve(undefined);
|
||||
await release.catch(() => undefined);
|
||||
}
|
||||
|
||||
expect(order).toEqual([
|
||||
"restore-started",
|
||||
@@ -738,25 +767,36 @@ it("waits for a cancelled environment mutation before restoring it", async () =>
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const opening = fixture.open(controller.signal);
|
||||
await localeStarted.promise;
|
||||
const cancelled = expect(opening).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
controller.abort();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(fixture.environmentSession.send).not.toHaveBeenCalledWith(
|
||||
"Emulation.setLocaleOverride",
|
||||
{ locale: "" },
|
||||
);
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
localeStarted.promise,
|
||||
opening,
|
||||
"locale mutation",
|
||||
);
|
||||
const cancelled = expect(opening).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
controller.abort();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(fixture.environmentSession.send).not.toHaveBeenCalledWith(
|
||||
"Emulation.setLocaleOverride",
|
||||
{ locale: "" },
|
||||
);
|
||||
|
||||
localeResult.resolve(undefined);
|
||||
await cancelled;
|
||||
expect(fixture.environmentSession.send).toHaveBeenCalledWith(
|
||||
"Emulation.setLocaleOverride",
|
||||
{ locale: "" },
|
||||
);
|
||||
expect(fixture.environmentSession.detach).toHaveBeenCalledOnce();
|
||||
expect(fixture.browser.close).toHaveBeenCalledOnce();
|
||||
localeResult.resolve(undefined);
|
||||
await cancelled;
|
||||
expect(fixture.environmentSession.send).toHaveBeenCalledWith(
|
||||
"Emulation.setLocaleOverride",
|
||||
{ locale: "" },
|
||||
);
|
||||
expect(fixture.environmentSession.detach).toHaveBeenCalledOnce();
|
||||
expect(fixture.browser.close).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
controller.abort();
|
||||
localeResult.resolve(undefined);
|
||||
const opened = await opening.catch(() => undefined);
|
||||
await opened?.cleanup.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("releases cleanup ownership when the attached target is destroyed", async () => {
|
||||
@@ -1055,12 +1095,26 @@ it("rechecks the browser release gate after finalization changes its revision",
|
||||
},
|
||||
);
|
||||
|
||||
await eventsStarted.promise;
|
||||
finishEvents.resolve(undefined);
|
||||
await gateStarted.promise;
|
||||
revision += 1;
|
||||
finishGate.resolve(undefined);
|
||||
await release;
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
eventsStarted.promise,
|
||||
release,
|
||||
"browser event finalization",
|
||||
);
|
||||
finishEvents.resolve(undefined);
|
||||
await awaitAcknowledgement(
|
||||
gateStarted.promise,
|
||||
release,
|
||||
"browser release gate",
|
||||
);
|
||||
revision += 1;
|
||||
finishGate.resolve(undefined);
|
||||
await release;
|
||||
} finally {
|
||||
finishEvents.resolve(undefined);
|
||||
finishGate.resolve(undefined);
|
||||
await release.catch(() => undefined);
|
||||
}
|
||||
|
||||
expect(gateRuns).toEqual([0, 1]);
|
||||
expect(closeBrowser).toHaveBeenCalledTimes(1);
|
||||
@@ -1092,10 +1146,19 @@ it("runs the latest stop gate after attached restoration and before disconnect",
|
||||
},
|
||||
});
|
||||
|
||||
await restorationStarted.promise;
|
||||
revision += 1;
|
||||
finishRestoration.resolve(undefined);
|
||||
await release;
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
restorationStarted.promise,
|
||||
release,
|
||||
"browser restoration",
|
||||
);
|
||||
revision += 1;
|
||||
finishRestoration.resolve(undefined);
|
||||
await release;
|
||||
} finally {
|
||||
finishRestoration.resolve(undefined);
|
||||
await release.catch(() => undefined);
|
||||
}
|
||||
|
||||
expect(gateRevisions).toEqual([1]);
|
||||
expect(disconnected).toBe(true);
|
||||
@@ -1143,19 +1206,32 @@ it.each([false, true])(
|
||||
() => ({ ok: true as const }),
|
||||
(cause: unknown) => ({ ok: false as const, cause }),
|
||||
);
|
||||
await gateStarted.promise;
|
||||
if (changeRevision) revision += 1;
|
||||
finishGate.resolve(undefined);
|
||||
await expect(release).resolves.toEqual({ ok: false, cause: failure });
|
||||
expect(closeBrowser).toHaveBeenCalledTimes(1);
|
||||
expect(transportClosed).toBe(false);
|
||||
expect(owner.browserClosed).toBe(false);
|
||||
expect(owner.cleanupResources).toContain("browser_transport");
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
gateStarted.promise,
|
||||
release,
|
||||
"browser release gate",
|
||||
);
|
||||
if (changeRevision) revision += 1;
|
||||
finishGate.resolve(undefined);
|
||||
await expect(release).resolves.toEqual({ ok: false, cause: failure });
|
||||
expect(closeBrowser).toHaveBeenCalledTimes(1);
|
||||
expect(transportClosed).toBe(false);
|
||||
expect(owner.browserClosed).toBe(false);
|
||||
expect(owner.cleanupResources).toContain("browser_transport");
|
||||
|
||||
await owner.release(undefined, gate);
|
||||
expect(closeBrowser).toHaveBeenCalledTimes(2);
|
||||
expect(transportClosed).toBe(true);
|
||||
expect(owner.browserClosed).toBe(true);
|
||||
await owner.release(undefined, gate);
|
||||
expect(closeBrowser).toHaveBeenCalledTimes(2);
|
||||
expect(transportClosed).toBe(true);
|
||||
expect(owner.browserClosed).toBe(true);
|
||||
} finally {
|
||||
finishGate.resolve(undefined);
|
||||
try {
|
||||
await release;
|
||||
} finally {
|
||||
await owner.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1076,6 +1076,12 @@ describe("saved response JSON pointers", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("selects the root and decodes escaped response keys once", () => {
|
||||
const value = { "a/b": { "~1": "literal", "/": "slash" } };
|
||||
expect(responsePointerValue(value, "")).toBe(value);
|
||||
expect(responsePointerValue(value, "/a~1b/~01")).toBe("literal");
|
||||
});
|
||||
|
||||
it("does not coerce a terminal newline in an array index", () => {
|
||||
expect(responsePointerValue({ items: ["record-7"] }, "/items/0\n")).toBe(
|
||||
undefined,
|
||||
|
||||
@@ -389,14 +389,10 @@ export class PlaywrightScenarioServiceWorkers {
|
||||
// Abort discovery/handshakes and reads, never an already-sent page mutation.
|
||||
this.#setup.abort();
|
||||
const restoration = (this.#restoration ??= this.restore());
|
||||
void restoration.then(
|
||||
() => {
|
||||
if (this.#restoration === restoration) this.#restoration = undefined;
|
||||
},
|
||||
() => {
|
||||
if (this.#restoration === restoration) this.#restoration = undefined;
|
||||
},
|
||||
);
|
||||
const clearRestoration = (): void => {
|
||||
if (this.#restoration === restoration) this.#restoration = undefined;
|
||||
};
|
||||
void restoration.then(clearRestoration, clearRestoration);
|
||||
// Restoration completion does not discharge a stop request that arrived
|
||||
// after that restoration began. Both paths share the owner's stop latch.
|
||||
const completion = restoration.then(() => this.fulfillStopRequest());
|
||||
@@ -427,14 +423,10 @@ export class PlaywrightScenarioServiceWorkers {
|
||||
this.#loadingStopped = true;
|
||||
})();
|
||||
this.#stopPromise = stopping;
|
||||
void stopping.then(
|
||||
() => {
|
||||
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
|
||||
},
|
||||
() => {
|
||||
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
|
||||
},
|
||||
);
|
||||
const clearStop = (): void => {
|
||||
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
|
||||
};
|
||||
void stopping.then(clearStop, clearStop);
|
||||
return stopping;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import {
|
||||
AnalysisCleanupLeaseGroup,
|
||||
AnalysisCleanupLease,
|
||||
@@ -550,24 +551,34 @@ it("transfers cancellation to a no-stop cleanup already in progress", async () =
|
||||
});
|
||||
|
||||
const firstClose = session.close(false);
|
||||
await started;
|
||||
cancellation.abort();
|
||||
const retry = session.close();
|
||||
finishRestoration();
|
||||
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
|
||||
"disconnected-external",
|
||||
"disconnected-external",
|
||||
]);
|
||||
try {
|
||||
await awaitAcknowledgement(started, firstClose, "policy restoration");
|
||||
cancellation.abort();
|
||||
const retry = session.close();
|
||||
finishRestoration();
|
||||
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
|
||||
"disconnected-external",
|
||||
"disconnected-external",
|
||||
]);
|
||||
|
||||
const policyStops = mocks.policySend.mock.calls.filter(
|
||||
([method]) => method === "Page.stopLoading",
|
||||
).length;
|
||||
const frameStops = page.evaluate.mock.calls.filter(
|
||||
([script]) => script === "globalThis.stop()",
|
||||
).length;
|
||||
expect(policyStops + frameStops).toBe(1);
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
expect(browserWasOpenAtStop).toBe(true);
|
||||
const policyStops = mocks.policySend.mock.calls.filter(
|
||||
([method]) => method === "Page.stopLoading",
|
||||
).length;
|
||||
const frameStops = page.evaluate.mock.calls.filter(
|
||||
([script]) => script === "globalThis.stop()",
|
||||
).length;
|
||||
expect(policyStops + frameStops).toBe(1);
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
expect(browserWasOpenAtStop).toBe(true);
|
||||
} finally {
|
||||
cancellation.abort();
|
||||
finishRestoration();
|
||||
try {
|
||||
await firstClose;
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("does not repeat a stop confirmed by storage during a late retry", async () => {
|
||||
@@ -591,27 +602,37 @@ it("does not repeat a stop confirmed by storage during a late retry", async () =
|
||||
});
|
||||
|
||||
const firstClose = session.close(false);
|
||||
await started;
|
||||
cancellation.abort();
|
||||
const retry = session.close();
|
||||
finishRestoration();
|
||||
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
|
||||
"disconnected-external",
|
||||
"disconnected-external",
|
||||
]);
|
||||
try {
|
||||
await awaitAcknowledgement(started, firstClose, "policy restoration");
|
||||
cancellation.abort();
|
||||
const retry = session.close();
|
||||
finishRestoration();
|
||||
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
|
||||
"disconnected-external",
|
||||
"disconnected-external",
|
||||
]);
|
||||
|
||||
expect(storage?.close).toHaveBeenCalledWith(false);
|
||||
expect(
|
||||
mocks.policySend.mock.calls.filter(
|
||||
([method]) => method === "Page.stopLoading",
|
||||
),
|
||||
).toHaveLength(0);
|
||||
expect(
|
||||
page.evaluate.mock.calls.filter(
|
||||
([script]) => script === "globalThis.stop()",
|
||||
),
|
||||
).toHaveLength(0);
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
expect(storage?.close).toHaveBeenCalledWith(false);
|
||||
expect(
|
||||
mocks.policySend.mock.calls.filter(
|
||||
([method]) => method === "Page.stopLoading",
|
||||
),
|
||||
).toHaveLength(0);
|
||||
expect(
|
||||
page.evaluate.mock.calls.filter(
|
||||
([script]) => script === "globalThis.stop()",
|
||||
),
|
||||
).toHaveLength(0);
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
cancellation.abort();
|
||||
finishRestoration();
|
||||
try {
|
||||
await firstClose;
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["block", "allow"] as const)(
|
||||
@@ -938,18 +959,19 @@ it("does not await policy initialization after its child target detaches", async
|
||||
},
|
||||
sessionId: "policy-session",
|
||||
});
|
||||
await started;
|
||||
const closing = session.close();
|
||||
mocks.policyEvent?.({
|
||||
method: "Target.detachedFromTarget",
|
||||
params: { sessionId: "child-session" },
|
||||
});
|
||||
try {
|
||||
await awaitAcknowledgement(started, held, "child policy admission");
|
||||
const closing = session.close();
|
||||
mocks.policyEvent?.({
|
||||
method: "Target.detachedFromTarget",
|
||||
params: { sessionId: "child-session" },
|
||||
});
|
||||
await expect(closing).resolves.toBe("disconnected-external");
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
releaseChild();
|
||||
await session.close();
|
||||
}
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips policy restoration for a child without a committed document", async () => {
|
||||
@@ -1222,7 +1244,11 @@ it.each([false, true])(
|
||||
signal: controller.signal,
|
||||
});
|
||||
try {
|
||||
await started.promise;
|
||||
await awaitAcknowledgement(
|
||||
started.promise,
|
||||
result,
|
||||
"cookie initialization",
|
||||
);
|
||||
controller.abort(reason);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(cleanup.close).not.toHaveBeenCalled();
|
||||
@@ -1396,14 +1422,21 @@ it("cancels a stalled policy preflight without allowing late setup", async () =>
|
||||
const outcome = expect(opening).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
await started;
|
||||
cancellation.abort();
|
||||
await outcome;
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
expect(cleanup.close.mock.calls[0]).toHaveLength(1);
|
||||
release();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(mocks.policySend).not.toHaveBeenCalled();
|
||||
try {
|
||||
await awaitAcknowledgement(started, opening, "policy preflight");
|
||||
cancellation.abort();
|
||||
await outcome;
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
expect(cleanup.close.mock.calls[0]).toHaveLength(1);
|
||||
release();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(mocks.policySend).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
cancellation.abort();
|
||||
release();
|
||||
await opening.catch(() => undefined);
|
||||
await outcome.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("waits for a pending registration installation before cancelled cleanup", async () => {
|
||||
@@ -1427,25 +1460,32 @@ it("waits for a pending registration installation before cancelled cleanup", asy
|
||||
const outcome = expect(opening).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
await started;
|
||||
cancellation.abort();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(cleanup.close).not.toHaveBeenCalled();
|
||||
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
||||
releaseInstallation();
|
||||
await outcome;
|
||||
const restoration = mocks.policySend.mock.calls.find(
|
||||
([method]) => method === "Runtime.evaluate",
|
||||
)?.[1];
|
||||
expect(restoration).toMatchObject({
|
||||
expression: expect.stringContaining("policy_owned"),
|
||||
});
|
||||
expect(mocks.policySend).toHaveBeenCalledWith(
|
||||
"Page.stopLoading",
|
||||
{},
|
||||
"policy-session",
|
||||
);
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
try {
|
||||
await awaitAcknowledgement(started, opening, "policy installation");
|
||||
cancellation.abort();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(cleanup.close).not.toHaveBeenCalled();
|
||||
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
||||
releaseInstallation();
|
||||
await outcome;
|
||||
const restoration = mocks.policySend.mock.calls.find(
|
||||
([method]) => method === "Runtime.evaluate",
|
||||
)?.[1];
|
||||
expect(restoration).toMatchObject({
|
||||
expression: expect.stringContaining("policy_owned"),
|
||||
});
|
||||
expect(mocks.policySend).toHaveBeenCalledWith(
|
||||
"Page.stopLoading",
|
||||
{},
|
||||
"policy-session",
|
||||
);
|
||||
expect(cleanup.close).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
cancellation.abort();
|
||||
releaseInstallation();
|
||||
await opening.catch(() => undefined);
|
||||
await outcome.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects attached block when caller-owned registrations already exist", async () => {
|
||||
@@ -1578,7 +1618,7 @@ it("retains late opening failure cleanup after cancellation until the exact prof
|
||||
const pending = provider.captureScenario(scenario, {
|
||||
signal: caller.signal,
|
||||
});
|
||||
await started;
|
||||
await awaitAcknowledgement(started, pending, "browser launch start");
|
||||
caller.abort(reason);
|
||||
const result = await pending;
|
||||
if (result.ok) throw new Error("Expected cancelled opening");
|
||||
@@ -1600,6 +1640,7 @@ it("retains late opening failure cleanup after cancellation until the exact prof
|
||||
});
|
||||
expect(failure.cleanupIncomplete).toBe(false);
|
||||
} finally {
|
||||
caller.abort(reason);
|
||||
denied = false;
|
||||
finishOpening();
|
||||
await provider.close();
|
||||
|
||||
@@ -120,11 +120,14 @@ it.for([
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await worker.close();
|
||||
preparation.mockRestore();
|
||||
child.stdin.destroy();
|
||||
child.stdout.destroy();
|
||||
child.stderr.destroy();
|
||||
try {
|
||||
await worker.close();
|
||||
} finally {
|
||||
preparation.mockRestore();
|
||||
child.stdin.destroy();
|
||||
child.stdout.destroy();
|
||||
child.stderr.destroy();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -601,84 +601,51 @@ it("does not mistake an independent body AbortError for a falsy caller cancellat
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps an actual stalled HTTP header deadline local to its map", async () => {
|
||||
const server = createServer(() => {});
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string")
|
||||
throw new Error("Expected TCP address");
|
||||
const url = `http://127.0.0.1:${address.port}/map`;
|
||||
try {
|
||||
const result = await fetchWebSourceMaps(
|
||||
[{ ...request, declaredUrl: url, fetchUrl: url }],
|
||||
{
|
||||
...input,
|
||||
allowed_origins: [new URL(url).origin],
|
||||
source_map_fetch_timeout_ms: 1000,
|
||||
},
|
||||
undefined,
|
||||
{ fetch },
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
items: [
|
||||
{
|
||||
status: "fetch_failed",
|
||||
limitation: expect.stringContaining("deadline"),
|
||||
},
|
||||
],
|
||||
},
|
||||
it.each(["headers", "body"] as const)(
|
||||
"keeps an actual stalled HTTP %s deadline local to its map",
|
||||
async (stage) => {
|
||||
const server = createServer((_request, response) => {
|
||||
if (stage === "body") {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.write('{"version":3,');
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an actual stalled HTTP response deadline local to its map", async () => {
|
||||
const server = createServer((_request, response) => {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.write('{"version":3,');
|
||||
});
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string")
|
||||
throw new Error("Expected TCP address");
|
||||
const url = `http://127.0.0.1:${address.port}/map`;
|
||||
try {
|
||||
const result = await fetchWebSourceMaps(
|
||||
[{ ...request, declaredUrl: url, fetchUrl: url }],
|
||||
{
|
||||
...input,
|
||||
allowed_origins: [new URL(url).origin],
|
||||
source_map_fetch_timeout_ms: 1000,
|
||||
},
|
||||
undefined,
|
||||
{ fetch },
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
items: [
|
||||
{
|
||||
status: "fetch_failed",
|
||||
limitation: expect.stringContaining("deadline"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
}
|
||||
});
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string")
|
||||
throw new Error("Expected TCP address");
|
||||
const url = `http://127.0.0.1:${address.port}/map`;
|
||||
try {
|
||||
const result = await fetchWebSourceMaps(
|
||||
[{ ...request, declaredUrl: url, fetchUrl: url }],
|
||||
{
|
||||
...input,
|
||||
allowed_origins: [new URL(url).origin],
|
||||
source_map_fetch_timeout_ms: 1000,
|
||||
},
|
||||
undefined,
|
||||
{ fetch },
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
items: [
|
||||
{
|
||||
status: "fetch_failed",
|
||||
limitation: expect.stringContaining("deadline"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([302, 503])(
|
||||
"retains HTTP %s response cleanup without hanging on an independent tee branch",
|
||||
|
||||
@@ -706,7 +706,8 @@ const fetchOne = async (
|
||||
budget: budget.decodedRecords,
|
||||
},
|
||||
);
|
||||
} catch (caught: unknown) {
|
||||
} catch (failure: unknown) {
|
||||
let caught = failure;
|
||||
if (caught instanceof SourceMapPendingFetch) {
|
||||
if (caught.cause instanceof SourceMapDeadlineError) {
|
||||
// Native fetch reports abort completion asynchronously. Check the next
|
||||
@@ -870,7 +871,8 @@ const readBoundedText = async (
|
||||
if (value !== undefined) chunks.push(value);
|
||||
bytes += nextBytes;
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
} catch (caught: unknown) {
|
||||
let cause = caught;
|
||||
// Fetch can error its body with the abort reason before read cancellation.
|
||||
// A confirmed errored stream is already terminal and needs only its lock released.
|
||||
await Promise.resolve();
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect, it } from "vitest";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
captureDecoderHeapContinuingRecovery,
|
||||
captureDecoderHeapRecovery,
|
||||
DEFAULT_CAPTURE_DECODER_HEAP,
|
||||
} from "../../config/captureDecoderHeap.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
|
||||
import { captureBudgets } from "./CaptureBudgets.js";
|
||||
import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
@@ -36,11 +37,8 @@ import {
|
||||
type CaptureDecoderHeap,
|
||||
} from "../../config/captureDecoderHeap.js";
|
||||
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
|
||||
import {
|
||||
NODE_CAPTURE_HEAP_ENFORCED,
|
||||
NODE_CAPTURE_HEAP_LIMITS,
|
||||
captureDecoderProcessMemoryLimitBytes,
|
||||
} from "./CaptureRelease.js";
|
||||
import { NODE_CAPTURE_HEAP_ENFORCED } from "./CaptureRelease.js";
|
||||
import { CAPTURE_DECODER_SEMI_SPACE_MIB } from "../../domain/analysisResourceLimits.js";
|
||||
const OPERATION = "inspect_web_network_capture";
|
||||
|
||||
/** Reason for a decoder whose owned process tree reached its Job Object commit limit. */
|
||||
@@ -296,7 +294,7 @@ export const captureMemoryFailure = (
|
||||
cause?: unknown,
|
||||
heap: CaptureDecoderHeap = DEFAULT_CAPTURE_DECODER_HEAP,
|
||||
): AnalysisResourceConstraintError => {
|
||||
const processMemoryLimitBytes = captureDecoderProcessMemoryLimitBytes(
|
||||
const processMemoryLimitBytes = javaScriptWorkerMemoryLimitBytes(
|
||||
heap.oldGenerationMiB,
|
||||
);
|
||||
const recovery = captureDecoderHeapRecovery(heap);
|
||||
@@ -315,7 +313,7 @@ export const captureMemoryFailure = (
|
||||
? {
|
||||
old_generation_heap_mib: heap.oldGenerationMiB,
|
||||
old_generation_heap_source: heap.source,
|
||||
semi_space_heap_mib: NODE_CAPTURE_HEAP_LIMITS.semiSpaceMiB,
|
||||
semi_space_heap_mib: CAPTURE_DECODER_SEMI_SPACE_MIB,
|
||||
}
|
||||
: {}),
|
||||
...(input.format !== "mitmproxy" && processMemoryLimitBytes !== null
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
|
||||
import { captureBudgets } from "./CaptureBudgets.js";
|
||||
import { visit } from "jsonc-parser";
|
||||
import { LosslessNumber } from "lossless-json";
|
||||
@@ -58,7 +59,7 @@ export const parseCaptureJson = (
|
||||
return `${parent.pointer}/${parent.items.length}`;
|
||||
return parent.pendingKey === undefined
|
||||
? parent.pointer
|
||||
: `${parent.pointer}/${parent.pendingKey.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
||||
: `${parent.pointer}/${escapeJsonPointerToken(parent.pendingKey)}`;
|
||||
};
|
||||
// The document root is container level 1. Admit a container only while
|
||||
// its own level stays within the budget; values inside an admitted
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
|
||||
import { isSafeNumber, type LosslessNumber } from "lossless-json";
|
||||
import type { WebNetworkCaptureRecord } from "../../domain/webNetworkCapture.js";
|
||||
import { parseRecordedTransportUrl } from "../../domain/recordedTransportUrl.js";
|
||||
@@ -149,7 +150,7 @@ export class CaptureRedaction {
|
||||
|
||||
/** Escape a producer property for an RFC 6901 location. */
|
||||
export const capturePointer = (parent: string, key: string): string =>
|
||||
`${parent}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
||||
`${parent}/${escapeJsonPointerToken(key)}`;
|
||||
|
||||
/** Structural record guard does not duck-type number/byte sidecar markers. */
|
||||
export const captureObject = (
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
import {
|
||||
CAPTURE_DECODER_SEMI_SPACE_MIB,
|
||||
DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB,
|
||||
} from "../../domain/analysisResourceLimits.js";
|
||||
import { javaScriptRuntime } from "../../domain/runtimeVersion.js";
|
||||
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
|
||||
|
||||
/**
|
||||
* Default V8 heap settings for the owned Node capture decoder process.
|
||||
* The old generation is `REA_CAPTURE_DECODER_OLD_GENERATION_MIB`; the
|
||||
* semi-space stays fixed.
|
||||
*/
|
||||
export const NODE_CAPTURE_HEAP_LIMITS = {
|
||||
oldGenerationMiB: DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB,
|
||||
semiSpaceMiB: CAPTURE_DECODER_SEMI_SPACE_MIB,
|
||||
} as const;
|
||||
|
||||
/** Windows Job Object commit limit for one resolved old-generation budget. */
|
||||
export const captureDecoderProcessMemoryLimitBytes = (
|
||||
oldGenerationMiB: number,
|
||||
): number | null => javaScriptWorkerMemoryLimitBytes(oldGenerationMiB);
|
||||
|
||||
/** The Node capture decoder runs on this executable; Bun accepts but ignores V8 heap flags. */
|
||||
export const NODE_CAPTURE_HEAP_ENFORCED =
|
||||
javaScriptRuntime(process.versions).id === "node";
|
||||
|
||||
/** Fixed offline interpretation profile, not an authenticated producer claim. */
|
||||
@@ -1,3 +1,4 @@
|
||||
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
|
||||
import { captureBudgets, withCaptureBudgets } from "./CaptureBudgets.js";
|
||||
import type { FileSnapshotPartialObservation } from "../../domain/fileSnapshotObservation.js";
|
||||
import {
|
||||
@@ -16,10 +17,7 @@ import {
|
||||
resolveCaptureDecoderHeap,
|
||||
type CaptureDecoderHeap,
|
||||
} from "../../config/captureDecoderHeap.js";
|
||||
import {
|
||||
NODE_CAPTURE_HEAP_ENFORCED,
|
||||
captureDecoderProcessMemoryLimitBytes,
|
||||
} from "./CaptureRelease.js";
|
||||
import { NODE_CAPTURE_HEAP_ENFORCED } from "./CaptureRelease.js";
|
||||
import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
@@ -613,9 +611,7 @@ const nodeMemoryLimitation = (heap: CaptureDecoderHeap): string => {
|
||||
const recovery = captureDecoderHeapRecovery(heap);
|
||||
if (NODE_CAPTURE_HEAP_ENFORCED)
|
||||
return `HAR/SAZ/qlog use a ${String(heap.oldGenerationMiB)} MiB Node old-generation heap (source ${heap.source}). ${recovery}`;
|
||||
const commitLimit = captureDecoderProcessMemoryLimitBytes(
|
||||
heap.oldGenerationMiB,
|
||||
);
|
||||
const commitLimit = javaScriptWorkerMemoryLimitBytes(heap.oldGenerationMiB);
|
||||
return commitLimit === null
|
||||
? `HAR/SAZ/qlog run on Bun, which does not enforce REA's V8 heap flags. ${recovery}`
|
||||
: `HAR/SAZ/qlog run on Bun, which does not enforce REA's V8 heap flags, under a ${String(commitLimit)}-byte Windows Job Object commit limit. ${recovery}`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import { NodeCaptureAdapter } from "./NodeCaptureAdapter.js";
|
||||
|
||||
@@ -9,17 +9,20 @@ it.each(["har", "qlog"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
vi.mock("./CaptureRelease.js", async (original) => ({
|
||||
...(await original<typeof import("./CaptureRelease.js")>()),
|
||||
NODE_CAPTURE_HEAP_ENFORCED: false,
|
||||
captureDecoderProcessMemoryLimitBytes: () => null,
|
||||
}));
|
||||
|
||||
it.each(["saz", "qlog"] as const)(
|
||||
"refuses a %s worker when the runtime has no enforceable memory bound",
|
||||
"uses an enforceable memory bound for %s on the current runtime",
|
||||
async (format) => {
|
||||
await expect(
|
||||
new NodeCaptureAdapter(format).command("/owned/request.json"),
|
||||
).rejects.toBeInstanceOf(AnalysisCapabilityUnavailableError);
|
||||
const adapter = new NodeCaptureAdapter(format);
|
||||
const bun = process.versions.bun !== undefined;
|
||||
if (bun && process.platform !== "win32") {
|
||||
await expect(
|
||||
adapter.command("/owned/request.json"),
|
||||
).rejects.toBeInstanceOf(AnalysisCapabilityUnavailableError);
|
||||
} else {
|
||||
const command = await adapter.command("/owned/request.json");
|
||||
expect(command.command).toBe(process.execPath);
|
||||
if (bun) expect(command.memoryLimitBytes).toBeGreaterThan(0);
|
||||
else expect(command.memoryLimitBytes).toBeUndefined();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js";
|
||||
import {
|
||||
@@ -6,10 +7,7 @@ import {
|
||||
type CaptureDecoderHeap,
|
||||
} from "../../config/captureDecoderHeap.js";
|
||||
import { CAPTURE_DECODER_SEMI_SPACE_MIB } from "../../domain/analysisResourceLimits.js";
|
||||
import {
|
||||
NODE_CAPTURE_HEAP_ENFORCED,
|
||||
captureDecoderProcessMemoryLimitBytes,
|
||||
} from "./CaptureRelease.js";
|
||||
import { NODE_CAPTURE_HEAP_ENFORCED } from "./CaptureRelease.js";
|
||||
import {
|
||||
QLOG_CAPTURE_PROVIDER_IDENTITY,
|
||||
HAR_CAPTURE_PROVIDER_IDENTITY,
|
||||
@@ -54,7 +52,7 @@ export class NodeCaptureAdapter implements HistoricalCaptureFormatAdapter {
|
||||
: [];
|
||||
}
|
||||
async command(requestPath: string) {
|
||||
const memoryLimitBytes = captureDecoderProcessMemoryLimitBytes(
|
||||
const memoryLimitBytes = javaScriptWorkerMemoryLimitBytes(
|
||||
this.heap.oldGenerationMiB,
|
||||
);
|
||||
if (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createDeferred } from "../../../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { readFile, rename, copyFile } from "node:fs/promises";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { ArtifactResourceScope } from "../../../artifacts/ArtifactResourceScope.js";
|
||||
@@ -240,7 +241,7 @@ it("bounds active imports without an unbounded queue and releases admission afte
|
||||
);
|
||||
const first = port.inspect(fixture.path);
|
||||
try {
|
||||
await reached.promise;
|
||||
await awaitAcknowledgement(reached.promise, first, "trace import");
|
||||
const second = await port.inspect(fixture.path);
|
||||
if (second.ok) throw new Error("Expected capacity refusal");
|
||||
expect(second.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { unescapeJsonPointerToken } from "../../../domain/jsonPointer.js";
|
||||
import { captureBudgets } from "../CaptureBudgets.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { CaptureFormatError } from "../CaptureFormatError.js";
|
||||
@@ -63,10 +64,7 @@ const excludeBody = (
|
||||
sha256: null,
|
||||
},
|
||||
);
|
||||
const parts = pointer
|
||||
.split("/")
|
||||
.slice(1)
|
||||
.map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
|
||||
const parts = pointer.split("/").slice(1).map(unescapeJsonPointerToken);
|
||||
let parent: JsonValue | undefined = record.reported;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
if (
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { evidenceSchema, type Evidence } from "../domain/evidence.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { ok } from "../domain/result.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
OBJDUMP_ARTIFACT_OPERATIONS,
|
||||
|
||||
@@ -49,11 +49,7 @@ export const registerUtilityCommands = (
|
||||
registerReferenceSourceCommand(cli, logger, environment);
|
||||
};
|
||||
|
||||
/** Decimal argv text; the shared deadline field owns the bounds and help. */
|
||||
const cliNativeDeadline = (field: typeof nativeUiDeadlineField) =>
|
||||
cliDecimalField(field);
|
||||
|
||||
// Decimal tokens only, for the same reason as cliNativeDeadline.
|
||||
// Decimal tokens only; shared fields own their bounds and help.
|
||||
const nativeUiScopeOptions = {
|
||||
pid: cliIntegerArgument(1).describe("Existing application PID"),
|
||||
windowId: cliIntegerArgument(1).describe(
|
||||
@@ -70,7 +66,7 @@ const nativeUiScopeOptions = {
|
||||
maxNodes: cliIntegerArgument(1)
|
||||
.default(500)
|
||||
.describe("Maximum accessibility nodes per capture"),
|
||||
nativeUiDeadlineMs: cliNativeDeadline(nativeUiDeadlineField),
|
||||
nativeUiDeadlineMs: cliDecimalField(nativeUiDeadlineField),
|
||||
};
|
||||
|
||||
const nativeUiScope = (options: {
|
||||
@@ -208,7 +204,7 @@ const registerNativeCommands = (
|
||||
path: localPathStringSchema.describe("Mach-O or app path"),
|
||||
}),
|
||||
options: z.object({
|
||||
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
|
||||
nativeCommandTimeoutMs: cliDecimalField(nativeCommandTimeoutField),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, command, () =>
|
||||
@@ -235,7 +231,7 @@ const registerNativeCommands = (
|
||||
.describe(
|
||||
"Plist path relative to the app root, or an absolute path (default: the bundle's Info.plist)",
|
||||
),
|
||||
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
|
||||
nativeCommandTimeoutMs: cliDecimalField(nativeCommandTimeoutField),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "inspect-plist", () =>
|
||||
@@ -263,7 +259,7 @@ const registerNativeCommands = (
|
||||
symbols: swiftSymbolsSchema.describe("Swift mangled symbols to demangle"),
|
||||
}),
|
||||
options: z.object({
|
||||
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
|
||||
nativeCommandTimeoutMs: cliDecimalField(nativeCommandTimeoutField),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "demangle-swift", () =>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createWebRuntimeService } from "../composition/webRuntime.js";
|
||||
import { cliDomainInput } from "./domainInput.js";
|
||||
import { observeWebExecutionInputSchema } from "../domain/webExecution.js";
|
||||
import { inspectWebEventListenersInputSchema } from "../domain/webEventListeners.js";
|
||||
import { browserCliError } from "./browserCliError.js";
|
||||
import {
|
||||
browserScopeOptions,
|
||||
selectedCommandTimeout,
|
||||
|
||||
+1
-3
@@ -35,7 +35,6 @@ export const parseCliJsonInput = async (
|
||||
options?: {
|
||||
readonly snapshot?: boolean;
|
||||
readonly stdin?: Readable;
|
||||
readonly maxStdinBytes?: number;
|
||||
},
|
||||
): Promise<
|
||||
| { readonly ok: true; readonly value: unknown }
|
||||
@@ -132,13 +131,12 @@ const readCliJsonStdin = async (
|
||||
options?: {
|
||||
readonly snapshot?: boolean;
|
||||
readonly stdin?: Readable;
|
||||
readonly maxStdinBytes?: number;
|
||||
},
|
||||
): Promise<
|
||||
Result<unknown, AnalysisInputError | AnalysisResourceConstraintError>
|
||||
> => {
|
||||
const stream = options?.stdin ?? process.stdin;
|
||||
const maxBytes = options?.maxStdinBytes ?? DEFAULT_MAX_STDIN_BYTES;
|
||||
const maxBytes = DEFAULT_MAX_STDIN_BYTES;
|
||||
const chunks: Buffer[] = [];
|
||||
let totalBytes = 0;
|
||||
const requireHeadroom = (): void => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { statSync } from "node:fs";
|
||||
import { win32 } from "node:path";
|
||||
import { windowsPathEntries } from "../process/WindowsExecutable.js";
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface WindowsExecutableLookup {
|
||||
|
||||
const isRegularFile = (path: string): boolean => {
|
||||
try {
|
||||
return existsSync(path) && statSync(path).isFile();
|
||||
return statSync(path).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { analyzeCapturedWebScript } from "../domain/webBundleAnalyzer.js";
|
||||
import {
|
||||
serveWebBundleAnalysisWorker,
|
||||
type WebBundleScriptAnalyzer,
|
||||
} from "../browser/WebBundleAnalysisWorkerServer.js";
|
||||
|
||||
const analyze: WebBundleScriptAnalyzer = (script, precision) =>
|
||||
analyzeCapturedWebScript(script, precision);
|
||||
import { serveWebBundleAnalysisWorker } from "../browser/WebBundleAnalysisWorkerServer.js";
|
||||
|
||||
try {
|
||||
await serveWebBundleAnalysisWorker(analyze);
|
||||
await serveWebBundleAnalysisWorker(analyzeCapturedWebScript);
|
||||
} catch (cause: unknown) {
|
||||
process.stderr.write(
|
||||
`${cause instanceof Error ? cause.message : String(cause)}\n`,
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
resolveRegexWorkerHeapRequest,
|
||||
REGEX_WORKER_HEAP_REQUEST_SETTING,
|
||||
regexWorkerHeapRequestContinuingRecovery,
|
||||
regexWorkerHeapRequestRecovery,
|
||||
} from "./regexWorkerHeapRequest.js";
|
||||
import {
|
||||
MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB,
|
||||
|
||||
+67
-39
@@ -4,16 +4,73 @@ import { expect, it } from "vitest";
|
||||
import { toolContract } from "./toolContracts.js";
|
||||
import { toolInputSchemaWithMetadata } from "./toolSchemaMetadata.js";
|
||||
|
||||
const view = {
|
||||
kind: "beam",
|
||||
collection: "exports",
|
||||
fields: ["function"],
|
||||
const beamView = { kind: "beam", collection: "exports", fields: ["function"] };
|
||||
const pycView = {
|
||||
kind: "python-pyc",
|
||||
collection: "code_objects",
|
||||
fields: ["name"],
|
||||
};
|
||||
|
||||
it.each(["draft-2020-12", "draft-07"] as const)(
|
||||
"advertises BEAM delivery combinations in %s",
|
||||
(target) => {
|
||||
const contract = toolContract("inspect_erlang_beam");
|
||||
const cases = [
|
||||
{
|
||||
name: "inspect_erlang_beam",
|
||||
path: "/tmp/module.beam",
|
||||
view: beamView,
|
||||
extra: [
|
||||
[{ detail: "summary", view: { ...beamView, fields: ["name"] } }, false],
|
||||
[
|
||||
{ detail: "summary", view: { ...beamView, modules: ["erlang"] } },
|
||||
false,
|
||||
],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...beamView, collection: "imports", modules: ["erlang"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...beamView, collection: "chunks", fields: ["payload"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...beamView, collection: "atoms", fields: ["index", "name"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...beamView, collection: "lambdas", fields: ["free_vars"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "inspect_python_pyc",
|
||||
path: "/tmp/module.pyc",
|
||||
view: pycView,
|
||||
extra: [],
|
||||
},
|
||||
] as const;
|
||||
|
||||
it.each(
|
||||
cases.flatMap((testCase) =>
|
||||
(["draft-2020-12", "draft-07"] as const).map((target) => ({
|
||||
...testCase,
|
||||
target,
|
||||
})),
|
||||
),
|
||||
)(
|
||||
"advertises $name delivery combinations in $target",
|
||||
({ name, path, view, extra, target }) => {
|
||||
const contract = toolContract(name);
|
||||
const projected = toolInputSchemaWithMetadata(contract)[
|
||||
"~standard"
|
||||
].jsonSchema.input({ target });
|
||||
@@ -43,38 +100,9 @@ it.each(["draft-2020-12", "draft-07"] as const)(
|
||||
[{ detail: "summary", view: { ...view, fields: ["missing"] } }, false],
|
||||
[{ detail: "summary", view: { ...view, kind: "native-exports" } }, false],
|
||||
[{ detail: "summary", view, unexpected: true }, false],
|
||||
[{ detail: "summary", view: { ...view, fields: ["name"] } }, false],
|
||||
[{ detail: "summary", view: { ...view, modules: ["erlang"] } }, false],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...view, collection: "imports", modules: ["erlang"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...view, collection: "chunks", fields: ["payload"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...view, collection: "atoms", fields: ["index", "name"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
detail: "summary",
|
||||
view: { ...view, collection: "lambdas", fields: ["free_vars"] },
|
||||
},
|
||||
true,
|
||||
],
|
||||
...extra,
|
||||
] as const) {
|
||||
const input = { path: "/tmp/module.beam", ...delivery };
|
||||
const input = { path, ...delivery };
|
||||
expect(
|
||||
contract.inputSchema.safeParse(input).success,
|
||||
JSON.stringify(input),
|
||||
Loaded 100 of 369 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user