mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b13dfec54 | ||
|
|
e7cbc046d1 | ||
|
|
d1e7e4d375 | ||
|
|
ac76567498 | ||
|
|
883f3460d6 | ||
|
|
b491c5d7dd |
No files matched your search
@@ -651,6 +651,13 @@ be verified. See [MCP contracts](../reference/mcp-contracts.md) for session life
|
|||||||
[Ghidra first-query deadlines](../reference/mcp-contracts.md#ghidra-first-query-deadlines-and-recovery)
|
[Ghidra first-query deadlines](../reference/mcp-contracts.md#ghidra-first-query-deadlines-and-recovery)
|
||||||
for import, client timeouts and recovery.
|
for import, client timeouts and recovery.
|
||||||
|
|
||||||
|
One-shot binary analysis validates the selected operation's input before creating
|
||||||
|
a provider session, opening the target, or replaying a snapshot. Invalid selectors
|
||||||
|
and option combinations report their input errors before target or provider
|
||||||
|
availability errors. An already-cancelled request keeps its cancellation reason.
|
||||||
|
Caller-selected paths and request parameters remain separate from parsed execution
|
||||||
|
arguments in Evidence.
|
||||||
|
|
||||||
A one-shot command attempts provider-session and runtime cleanup before reporting
|
A one-shot command attempts provider-session and runtime cleanup before reporting
|
||||||
a failure. If cleanup remains incomplete, inspect the reported resources and
|
a failure. If cleanup remains incomplete, inspect the reported resources and
|
||||||
follow the error's remediation. [Same-session cleanup retry](../reference/mcp-contracts.md#retained-cleanup-retry),
|
follow the error's remediation. [Same-session cleanup retry](../reference/mcp-contracts.md#retained-cleanup-retry),
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ import {
|
|||||||
type MachoLoadCommandTableHeader,
|
type MachoLoadCommandTableHeader,
|
||||||
} from "../domain/apple/machoLoadCommands.js";
|
} from "../domain/apple/machoLoadCommands.js";
|
||||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||||
|
import { localPathStringSchema } from "../domain/localPath.js";
|
||||||
import type { JsonValue } from "../domain/jsonValue.js";
|
import type { JsonValue } from "../domain/jsonValue.js";
|
||||||
|
|
||||||
const XML_PLIST_PROBE_CHUNK_BYTES = 64 * 1024;
|
const XML_PLIST_PROBE_CHUNK_BYTES = 64 * 1024;
|
||||||
@@ -173,9 +174,25 @@ export const parseBinaryTarget = async (
|
|||||||
): Promise<
|
): Promise<
|
||||||
Result<BinaryTarget, BinaryTargetError | AnalysisCancelledError>
|
Result<BinaryTarget, BinaryTargetError | AnalysisCancelledError>
|
||||||
> => {
|
> => {
|
||||||
const cwd = options.cwd ?? process.cwd();
|
|
||||||
const { signal } = options;
|
const { signal } = options;
|
||||||
const candidate = absoluteCallerPath(input, { cwd });
|
const admitted = localPathStringSchema.safeParse(input);
|
||||||
|
if (!admitted.success) {
|
||||||
|
if (signal?.aborted === true)
|
||||||
|
return err(
|
||||||
|
new AnalysisCancelledError("resolve_binary_target", {
|
||||||
|
cause: signal.reason,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return err(
|
||||||
|
new BinaryTargetError(
|
||||||
|
input,
|
||||||
|
admitted.error.issues[0]?.message ?? "Invalid local path",
|
||||||
|
{ cause: admitted.error },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const cwd = options.cwd ?? process.cwd();
|
||||||
|
const candidate = absoluteCallerPath(admitted.data, { cwd });
|
||||||
// Failures before canonicalization concern the selected path itself; later
|
// Failures before canonicalization concern the selected path itself; later
|
||||||
// failures concern the canonical path opened with symbolic links rejected.
|
// failures concern the canonical path opened with symbolic links rejected.
|
||||||
let stage: TargetFilesystemStage = "resolve";
|
let stage: TargetFilesystemStage = "resolve";
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
admitOneShotOperation,
|
||||||
|
type DirectAnalysisTool,
|
||||||
|
} from "./oneShotOperationInput.js";
|
||||||
import { admitCompletedAnalysis } from "./investigation/admitCompletedAnalysis.js";
|
import { admitCompletedAnalysis } from "./investigation/admitCompletedAnalysis.js";
|
||||||
import type { EspFlashSelection } from "../domain/firmware/espFlashLocation.js";
|
import type { EspFlashSelection } from "../domain/firmware/espFlashLocation.js";
|
||||||
import type { ExecutableFormatHint } from "../domain/dosCom.js";
|
import type { ExecutableFormatHint } from "../domain/dosCom.js";
|
||||||
@@ -96,30 +100,6 @@ import {
|
|||||||
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
|
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
|
||||||
import { BUNDLE_RESOURCE_OPERATIONS } from "./binary/BundleResourceSession.js";
|
import { BUNDLE_RESOURCE_OPERATIONS } from "./binary/BundleResourceSession.js";
|
||||||
|
|
||||||
type DirectAnalysisTool =
|
|
||||||
| "annotate_native_function"
|
|
||||||
| "inspect_native_load_image"
|
|
||||||
| "read_bytes"
|
|
||||||
| "search_bytes"
|
|
||||||
| "address_to_file_offset"
|
|
||||||
| "binary_overview"
|
|
||||||
| "procedure_pseudo_code"
|
|
||||||
| "procedure_references"
|
|
||||||
| "read_function_instructions"
|
|
||||||
| "inspect_native_instruction"
|
|
||||||
| "inspect_native_data_type"
|
|
||||||
| "resolve_native_call_targets"
|
|
||||||
| "analyze_function"
|
|
||||||
| "inspect_native_api"
|
|
||||||
| "inspect_native_dispatch_metadata"
|
|
||||||
| "search_strings"
|
|
||||||
| "search_procedures"
|
|
||||||
| "xrefs"
|
|
||||||
| "trace_feature"
|
|
||||||
| "trace_native_ui_action"
|
|
||||||
| "trace_native_values"
|
|
||||||
| "decompile_all";
|
|
||||||
|
|
||||||
// These one-shot CLI questions require native utilities, not a deep engine.
|
// These one-shot CLI questions require native utilities, not a deep engine.
|
||||||
// Runtime observation retains the ordinary session and authority checks.
|
// Runtime observation retains the ordinary session and authority checks.
|
||||||
const NATIVE_INSPECTION_OPERATIONS: ReadonlySet<string> = new Set([
|
const NATIVE_INSPECTION_OPERATIONS: ReadonlySet<string> = new Set([
|
||||||
@@ -158,20 +138,30 @@ export const runDirectAnalysis = async (
|
|||||||
readonly includeEvidenceSources?: boolean;
|
readonly includeEvidenceSources?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<AnalysisResultWithSources<Evidence, JsonValue>> =>
|
): Promise<AnalysisResultWithSources<Evidence, JsonValue>> =>
|
||||||
withProcessCancellation(options.signal, (signal) =>
|
withProcessCancellation(options.signal, (signal) => {
|
||||||
runAnalysis(dependencies, path, tool, arguments_, {
|
const admitted = admitOneShotOperation(tool, arguments_, signal);
|
||||||
logger: options.logger ?? silentLogger,
|
if (!admitted.ok)
|
||||||
snapshotPath: options.snapshotPath,
|
return Promise.resolve(err(cliError(admitted.error, path)));
|
||||||
signal,
|
return runAnalysis(
|
||||||
...(options.includeEvidenceSources === true
|
dependencies,
|
||||||
? { includeEvidenceSources: true }
|
path,
|
||||||
: {}),
|
admitted.value.name,
|
||||||
...targetSelection(options),
|
admitted.value.input,
|
||||||
...(options.providerId === undefined
|
{
|
||||||
? {}
|
evidenceParameters: arguments_,
|
||||||
: { providerId: options.providerId }),
|
logger: options.logger ?? silentLogger,
|
||||||
}),
|
snapshotPath: options.snapshotPath,
|
||||||
);
|
signal,
|
||||||
|
...(options.includeEvidenceSources === true
|
||||||
|
? { includeEvidenceSources: true }
|
||||||
|
: {}),
|
||||||
|
...targetSelection(options),
|
||||||
|
...(options.providerId === undefined
|
||||||
|
? {}
|
||||||
|
: { providerId: options.providerId }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/** How the caller asks an artifact target to be read. */
|
/** How the caller asks an artifact target to be read. */
|
||||||
export interface ArtifactTargetSelection {
|
export interface ArtifactTargetSelection {
|
||||||
@@ -268,8 +258,7 @@ export const runProviderAnalysis = async (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
return withProcessCancellation(signal, async (operationSignal) => {
|
return withProcessCancellation(signal, async (operationSignal) => {
|
||||||
// The selected CLI path is a request fact. Keep it out of provider inputs
|
// Preserve the selected path and original arguments as request facts.
|
||||||
// and target identity, which may use the resolved executable instead.
|
|
||||||
const evidenceParameters = { ...arguments_, input_path: path };
|
const evidenceParameters = { ...arguments_, input_path: path };
|
||||||
if (isManagedToolName(tool)) {
|
if (isManagedToolName(tool)) {
|
||||||
const original = await runManagedProviderAnalysis(
|
const original = await runManagedProviderAnalysis(
|
||||||
@@ -309,18 +298,26 @@ export const runProviderAnalysis = async (
|
|||||||
);
|
);
|
||||||
return result.ok ? ok(result.value) : err(cliError(result.error, path));
|
return result.ok ? ok(result.value) : err(cliError(result.error, path));
|
||||||
}
|
}
|
||||||
return runAnalysis(dependencies, path, tool, arguments_, {
|
const admitted = admitOneShotOperation(tool, arguments_, operationSignal);
|
||||||
logger,
|
if (!admitted.ok) return err(cliError(admitted.error, path));
|
||||||
snapshotPath: undefined,
|
return runAnalysis(
|
||||||
signal: operationSignal,
|
dependencies,
|
||||||
evidenceParameters,
|
path,
|
||||||
...targetSelection(selection ?? {}),
|
admitted.value.name,
|
||||||
...(ARTIFACT_ANALYSIS_OPERATION_SET.has(tool)
|
admitted.value.input,
|
||||||
? { createSession: dependencies.createArtifactBinarySession }
|
{
|
||||||
: NATIVE_INSPECTION_OPERATIONS.has(tool)
|
logger,
|
||||||
? { createSession: dependencies.createNativeBinarySession }
|
snapshotPath: undefined,
|
||||||
: {}),
|
signal: operationSignal,
|
||||||
});
|
evidenceParameters,
|
||||||
|
...targetSelection(selection ?? {}),
|
||||||
|
...(ARTIFACT_ANALYSIS_OPERATION_SET.has(tool)
|
||||||
|
? { createSession: dependencies.createArtifactBinarySession }
|
||||||
|
: NATIVE_INSPECTION_OPERATIONS.has(tool)
|
||||||
|
? { createSession: dependencies.createNativeBinarySession }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -334,6 +331,8 @@ export const runManagedProviderExecution = async (
|
|||||||
recoveryParameters?: Readonly<Record<string, JsonValue>>,
|
recoveryParameters?: Readonly<Record<string, JsonValue>>,
|
||||||
): Promise<Result<AnalysisExecution, AnalysisError>> =>
|
): Promise<Result<AnalysisExecution, AnalysisError>> =>
|
||||||
withProcessCancellation(signal, async (operationSignal) => {
|
withProcessCancellation(signal, async (operationSignal) => {
|
||||||
|
const admitted = admitOneShotOperation(tool, parameters, operationSignal);
|
||||||
|
if (!admitted.ok) return admitted;
|
||||||
const session = await dependencies.createManagedBinarySession();
|
const session = await dependencies.createManagedBinarySession();
|
||||||
return withSessionCleanup(
|
return withSessionCleanup(
|
||||||
session,
|
session,
|
||||||
@@ -341,10 +340,12 @@ export const runManagedProviderExecution = async (
|
|||||||
const opened = await session.open(path, { signal: operationSignal });
|
const opened = await session.open(path, { signal: operationSignal });
|
||||||
if (!opened.ok) return opened;
|
if (!opened.ok) return opened;
|
||||||
return recoveryParameters === undefined
|
return recoveryParameters === undefined
|
||||||
? await session.execute(tool, parameters, { signal: operationSignal })
|
? await session.execute(admitted.value.name, admitted.value.input, {
|
||||||
|
signal: operationSignal,
|
||||||
|
})
|
||||||
: await session.executeForDirectObservation(
|
: await session.executeForDirectObservation(
|
||||||
tool,
|
admitted.value.name,
|
||||||
parameters,
|
admitted.value.input,
|
||||||
recoveryParameters,
|
recoveryParameters,
|
||||||
{ signal: operationSignal },
|
{ signal: operationSignal },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { err, ok } from "../../domain/result.js";
|
|||||||
import { EvidenceFileError } from "../../domain/evidenceErrors.js";
|
import { EvidenceFileError } from "../../domain/evidenceErrors.js";
|
||||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||||
import type { Il2cppSeedWriterPort } from "./Il2cppSeedWriterPort.js";
|
import type { Il2cppSeedWriterPort } from "./Il2cppSeedWriterPort.js";
|
||||||
|
import { toolContract } from "../../contracts/toolContracts.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||||
|
|
||||||
const identity = {
|
const identity = {
|
||||||
id: "fixture-il2cpp",
|
id: "fixture-il2cpp",
|
||||||
@@ -21,6 +24,29 @@ const identity = {
|
|||||||
};
|
};
|
||||||
const report = createEmptyIl2cppMetadataFixture();
|
const report = createEmptyIl2cppMetadataFixture();
|
||||||
|
|
||||||
|
it.each([{ binary_architecture: "arm64" }, { generic_instances: true }])(
|
||||||
|
"binds binary-dependent selectors in the named input contract: %j",
|
||||||
|
(selection) => {
|
||||||
|
const schema = toolContract("inspect_il2cpp_metadata").inputSchema;
|
||||||
|
const validate = new Ajv2020({ strict: false }).compile(
|
||||||
|
z.toJSONSchema(schema, { io: "input" }),
|
||||||
|
);
|
||||||
|
const missingBinary = { path: report.artifact.path, ...selection };
|
||||||
|
const selectedBinary = {
|
||||||
|
...missingBinary,
|
||||||
|
binary_path: "/artifacts/GameAssembly.dll",
|
||||||
|
};
|
||||||
|
expect(schema.safeParse(missingBinary).success).toBe(false);
|
||||||
|
expect(validate(missingBinary)).toBe(false);
|
||||||
|
expect(schema.safeParse(selectedBinary).success).toBe(true);
|
||||||
|
expect(validate(selectedBinary)).toBe(true);
|
||||||
|
expect(
|
||||||
|
schema.safeParse({ path: report.artifact.path, generic_instances: false })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const serviceReturning = (value: Il2cppMetadata, calls = { count: 0 }) =>
|
const serviceReturning = (value: Il2cppMetadata, calls = { count: 0 }) =>
|
||||||
new Il2cppMetadataService({
|
new Il2cppMetadataService({
|
||||||
close: async () => {},
|
close: async () => {},
|
||||||
|
|||||||
@@ -106,32 +106,6 @@ export class Il2cppMetadataService {
|
|||||||
options?.signal,
|
options?.signal,
|
||||||
);
|
);
|
||||||
if (!input.ok) return input;
|
if (!input.ok) return input;
|
||||||
if (
|
|
||||||
input.value.binary_architecture !== undefined &&
|
|
||||||
input.value.binary_path === undefined
|
|
||||||
)
|
|
||||||
return err(
|
|
||||||
new AnalysisInputError(OPERATION, undefined, [
|
|
||||||
{
|
|
||||||
path: ["binary_architecture"],
|
|
||||||
reason: "invalid_value",
|
|
||||||
message: "binary_architecture selects a slice of binary_path.",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
input.value.generic_instances === true &&
|
|
||||||
input.value.binary_path === undefined
|
|
||||||
)
|
|
||||||
return err(
|
|
||||||
new AnalysisInputError(OPERATION, undefined, [
|
|
||||||
{
|
|
||||||
path: ["generic_instances"],
|
|
||||||
reason: "invalid_value",
|
|
||||||
message: "generic_instances links generic methods in binary_path.",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
if (output !== undefined) {
|
if (output !== undefined) {
|
||||||
if (input.value.binary_path === undefined)
|
if (input.value.binary_path === undefined)
|
||||||
return err(
|
return err(
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
|
import { toolContract } from "../contracts/toolContracts.js";
|
||||||
|
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/officialToolContracts.js";
|
||||||
|
import { NATIVE_TOOL_CONTRACTS } from "../contracts/native/nativeToolContracts.js";
|
||||||
|
import {
|
||||||
|
ARTIFACT_TOOL_CONTRACTS,
|
||||||
|
artifactInventoryInputSchema,
|
||||||
|
type ArtifactAnalysisOperation,
|
||||||
|
} from "../contracts/artifactToolContracts.js";
|
||||||
|
import {
|
||||||
|
MANAGED_TOOL_CONTRACTS,
|
||||||
|
managedArtifactInputSchema,
|
||||||
|
managedMembersInputSchema,
|
||||||
|
type ManagedToolName,
|
||||||
|
} from "../contracts/managed/managedToolContracts.js";
|
||||||
|
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
|
||||||
|
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
|
||||||
|
import type { JsonValue } from "../domain/jsonValue.js";
|
||||||
|
import { parseAnalysisInput } from "./AnalysisInput.js";
|
||||||
|
import { ok } from "../domain/result.js";
|
||||||
|
|
||||||
|
/** Operations the direct workflow can perform without a persistent session. */
|
||||||
|
export type DirectAnalysisTool =
|
||||||
|
| "annotate_native_function"
|
||||||
|
| "inspect_native_load_image"
|
||||||
|
| "read_bytes"
|
||||||
|
| "search_bytes"
|
||||||
|
| "address_to_file_offset"
|
||||||
|
| "binary_overview"
|
||||||
|
| "procedure_pseudo_code"
|
||||||
|
| "procedure_references"
|
||||||
|
| "read_function_instructions"
|
||||||
|
| "inspect_native_instruction"
|
||||||
|
| "inspect_native_data_type"
|
||||||
|
| "resolve_native_call_targets"
|
||||||
|
| "analyze_function"
|
||||||
|
| "inspect_native_api"
|
||||||
|
| "inspect_native_dispatch_metadata"
|
||||||
|
| "search_strings"
|
||||||
|
| "search_procedures"
|
||||||
|
| "xrefs"
|
||||||
|
| "trace_feature"
|
||||||
|
| "trace_native_ui_action"
|
||||||
|
| "trace_native_values"
|
||||||
|
| "decompile_all";
|
||||||
|
|
||||||
|
type OneShotOperation =
|
||||||
|
| DirectAnalysisTool
|
||||||
|
| NativeToolName
|
||||||
|
| ArtifactAnalysisOperation
|
||||||
|
| ManagedToolName;
|
||||||
|
type Schema = z.ZodType<Readonly<Record<string, unknown>>>;
|
||||||
|
|
||||||
|
type ContractSchemas<
|
||||||
|
Contracts extends readonly {
|
||||||
|
readonly name: string;
|
||||||
|
readonly inputSchema: Schema;
|
||||||
|
}[],
|
||||||
|
> = {
|
||||||
|
[Contract in Contracts[number] as Contract["name"]]: Contract["inputSchema"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const schemasByName = <
|
||||||
|
const Contracts extends readonly {
|
||||||
|
readonly name: string;
|
||||||
|
readonly inputSchema: Schema;
|
||||||
|
}[],
|
||||||
|
>(
|
||||||
|
contracts: Contracts,
|
||||||
|
): ContractSchemas<Contracts> =>
|
||||||
|
Object.fromEntries(
|
||||||
|
contracts.map(({ name, inputSchema }) => [name, inputSchema]),
|
||||||
|
) as ContractSchemas<Contracts>;
|
||||||
|
|
||||||
|
const schemas = {
|
||||||
|
...schemasByName(OFFICIAL_TOOL_CONTRACTS),
|
||||||
|
...schemasByName(NATIVE_TOOL_CONTRACTS),
|
||||||
|
...schemasByName(ARTIFACT_TOOL_CONTRACTS),
|
||||||
|
...schemasByName(MANAGED_TOOL_CONTRACTS),
|
||||||
|
...enhancedInputSchemas,
|
||||||
|
inspect_native_load_image: toolContract(
|
||||||
|
"inspect_native_load_image",
|
||||||
|
).inputSchema.omit({ detail: true }),
|
||||||
|
inspect_managed_artifact: managedArtifactInputSchema.omit({ detail: true }),
|
||||||
|
inspect_managed_members: managedMembersInputSchema.omit({ detail: true }),
|
||||||
|
inventory_artifact: artifactInventoryInputSchema,
|
||||||
|
} satisfies Record<OneShotOperation, Schema>;
|
||||||
|
|
||||||
|
/** Parse the named operation before target selection or snapshot replay. */
|
||||||
|
export const admitOneShotOperation = <Name extends OneShotOperation>(
|
||||||
|
name: Name,
|
||||||
|
input: Readonly<Record<string, JsonValue>>,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
) => {
|
||||||
|
type SelectedInput = Partial<z.output<(typeof schemas)[Name]>> &
|
||||||
|
Readonly<Record<string, JsonValue>>;
|
||||||
|
const parsed = parseAnalysisInput(
|
||||||
|
name,
|
||||||
|
input,
|
||||||
|
schemas[name] as Schema,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
if (!parsed.ok) return parsed;
|
||||||
|
// Admission owns the selected request: preserve top-level omissions for
|
||||||
|
// Evidence and snapshot identity while carrying each selected parsed value.
|
||||||
|
// Nested defaults remain in the owning field's parsed value.
|
||||||
|
const selected = Object.fromEntries(
|
||||||
|
Object.entries(parsed.value).filter(([key]) => Object.hasOwn(input, key)),
|
||||||
|
) as SelectedInput;
|
||||||
|
return ok({ name, input: selected });
|
||||||
|
};
|
||||||
@@ -20,11 +20,6 @@ it("reads a Python 3.9 reference-flagged PYZ table", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reads marshal one-byte string tags as Latin-1", () => {
|
|
||||||
for (const hex of ["6101000000e9", "4101000000e9", "7a01e9", "5a01e9"])
|
|
||||||
expect(readMarshalSubset(bytes(hex))).toEqual({ ok: true, value: "é" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("decodes CPython marshal Unicode with surrogatepass", () => {
|
it("decodes CPython marshal Unicode with surrogatepass", () => {
|
||||||
expect(
|
expect(
|
||||||
readMarshalSubset(
|
readMarshalSubset(
|
||||||
|
|||||||
@@ -168,8 +168,14 @@ const readAsciiBytes = (
|
|||||||
): Result<string, PyInstallerFailure> => {
|
): Result<string, PyInstallerFailure> => {
|
||||||
const bytes = readBytes(cursor, length);
|
const bytes = readBytes(cursor, length);
|
||||||
if (!bytes.ok) return bytes;
|
if (!bytes.ok) return bytes;
|
||||||
// These tags use CPython's one-byte Unicode constructor, not an ASCII check.
|
if (bytes.value.some((byte) => byte > 0x7f))
|
||||||
return ok(bytes.value.toString("latin1"));
|
return err(
|
||||||
|
pyInstallerFailure(
|
||||||
|
"format",
|
||||||
|
"PYZ marshal ASCII string contains a non-ASCII byte",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return ok(bytes.value.toString("ascii"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const readUtf8 = (
|
const readUtf8 = (
|
||||||
|
|||||||
@@ -215,8 +215,7 @@ export const registerBinaryDiagnosticsCommands = (
|
|||||||
maxIntegerTextBytes: cliDecimalField(
|
maxIntegerTextBytes: cliDecimalField(
|
||||||
inspectAppleIpsInputSchema.shape.max_integer_text_bytes,
|
inspectAppleIpsInputSchema.shape.max_integer_text_bytes,
|
||||||
),
|
),
|
||||||
joinPath: z
|
joinPath: localPathStringSchema
|
||||||
.string()
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Optional local Mach-O executable or dSYM whose slice LC_UUID matches report images",
|
"Optional local Mach-O executable or dSYM whose slice LC_UUID matches report images",
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ export const registerCaptureCommands = (
|
|||||||
path: ["all"],
|
path: ["all"],
|
||||||
message: "all and limit are mutually exclusive",
|
message: "all and limit are mutually exclusive",
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
not: {
|
||||||
|
required: ["all", "limit"],
|
||||||
|
properties: { all: { const: true } },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
run: ({ options: { all, producerLimits, ...options } }) =>
|
run: ({ options: { all, producerLimits, ...options } }) =>
|
||||||
withCommandCancellation((signal) =>
|
withCommandCancellation((signal) =>
|
||||||
@@ -119,7 +125,13 @@ export const registerCaptureCommands = (
|
|||||||
.refine(
|
.refine(
|
||||||
withinPacketCaptureSelectorBudget,
|
withinPacketCaptureSelectorBudget,
|
||||||
packetCaptureSelectorBudgetIssue,
|
packetCaptureSelectorBudgetIssue,
|
||||||
),
|
)
|
||||||
|
.meta({
|
||||||
|
not: {
|
||||||
|
required: ["all_matches", "match_limit"],
|
||||||
|
properties: { all_matches: { const: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
run: ({ args, options: { all_matches, producerLimits, ...options } }) =>
|
run: ({ args, options: { all_matches, producerLimits, ...options } }) =>
|
||||||
withCommandCancellation((signal) =>
|
withCommandCancellation((signal) =>
|
||||||
logCliCommand(logger, CLI_COMMANDS.inspectPacketCapture, async () => {
|
logCliCommand(logger, CLI_COMMANDS.inspectPacketCapture, async () => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { nativeInstructionInputSchema } from "../domain/native/nativeInstruction.js";
|
||||||
|
import { nativeFunctionAnnotationsInputSchema } from "../domain/native/nativeFunctionAnnotations.js";
|
||||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||||
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
|
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
|
||||||
import { nativeInvestigationTraceLimitsSchema } from "../domain/native/nativeInvestigationGraph.js";
|
import { nativeInvestigationTraceLimitsSchema } from "../domain/native/nativeInvestigationGraph.js";
|
||||||
@@ -395,7 +397,9 @@ export const registerCoreNativeCommands = (
|
|||||||
path: localPathStringSchema.describe(
|
path: localPathStringSchema.describe(
|
||||||
"Local native executable or app path",
|
"Local native executable or app path",
|
||||||
),
|
),
|
||||||
address: z.string().describe("Exact native address"),
|
address: nativeInstructionInputSchema.shape.address.describe(
|
||||||
|
"Exact native address",
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
options: z.object({
|
options: z.object({
|
||||||
...nativeOpenOptionFields,
|
...nativeOpenOptionFields,
|
||||||
@@ -557,11 +561,9 @@ const registerAnnotationCommand = (
|
|||||||
description: "Edit function annotations and return refreshed analysis",
|
description: "Edit function annotations and return refreshed analysis",
|
||||||
args: z.object({
|
args: z.object({
|
||||||
path: localPathStringSchema.describe("Local executable path"),
|
path: localPathStringSchema.describe("Local executable path"),
|
||||||
procedure: z
|
procedure: nativeFunctionAnnotationsInputSchema.shape.procedure.describe(
|
||||||
.string()
|
"Function name or address; Ghidra resolves interior addresses to the containing function and edits its entry",
|
||||||
.describe(
|
),
|
||||||
"Function name or address; Ghidra resolves interior addresses to the containing function and edits its entry",
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
options: z.object({
|
options: z.object({
|
||||||
name: z.string().min(1).optional().describe("Analyst function name"),
|
name: z.string().min(1).optional().describe("Analyst function name"),
|
||||||
|
|||||||
@@ -263,14 +263,12 @@ const withheldExportOptions = {
|
|||||||
.describe(
|
.describe(
|
||||||
"Seed the withheld files after all, which needs a seed_sign_in consent in --consent",
|
"Seed the withheld files after all, which needs a seed_sign_in consent in --consent",
|
||||||
),
|
),
|
||||||
consent: z
|
consent: localPathStringSchema
|
||||||
.string()
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"consent.json holding the person's consents, which the bundle ships",
|
"consent.json holding the person's consents, which the bundle ships",
|
||||||
),
|
),
|
||||||
withheldCopy: z
|
withheldCopy: localPathStringSchema
|
||||||
.string()
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"New or empty folder outside the bundle the withheld files are copied into, for whoever supplies them at run time",
|
"New or empty folder outside the bundle the withheld files are copied into, for whoever supplies them at run time",
|
||||||
@@ -340,8 +338,7 @@ const registerDesktopExportCommand = (
|
|||||||
.describe(
|
.describe(
|
||||||
"http or https URL the computer downloads the captured --package-file from when the bundle is put, for an application installed from a .deb file",
|
"http or https URL the computer downloads the captured --package-file from when the bundle is put, for an application installed from a .deb file",
|
||||||
),
|
),
|
||||||
downloadInstall: z
|
downloadInstall: localPathStringSchema
|
||||||
.string()
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"JSON file naming the AppImage or tarball a build computer unpacked the application from: kind, desktop_entry, url, sha256, architecture, and a tarball's program and name",
|
"JSON file naming the AppImage or tarball a build computer unpacked the application from: kind, desktop_entry, url, sha256, architecture, and a tarball's program and name",
|
||||||
@@ -358,8 +355,7 @@ const registerDesktopExportCommand = (
|
|||||||
.describe(
|
.describe(
|
||||||
"The seed was made for the task, as in a build computer, not taken from the user's own data; the README says so instead of keeping the bundle local",
|
"The seed was made for the task, as in a build computer, not taken from the user's own data; the README says so instead of keeping the bundle local",
|
||||||
),
|
),
|
||||||
screens: z
|
screens: localPathStringSchema
|
||||||
.string()
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Folder holding checkpoint screenshots for screen-judge examples.",
|
"Folder holding checkpoint screenshots for screen-judge examples.",
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||||
import { parseCliAnalysisEvidenceDelivery } from "./analysisEvidenceDelivery.js";
|
import { parseCliAnalysisEvidenceDelivery } from "./analysisEvidenceDelivery.js";
|
||||||
import { javaScriptRoutesDeliverySchema } from "../domain/analysisView/javascriptRoutesView.js";
|
import { javaScriptRoutesDeliverySchema } from "../domain/analysisView/javascriptRoutesView.js";
|
||||||
|
|
||||||
import { isAbsolute, resolve, sep } from "node:path";
|
import { isAbsolute, resolve, sep } from "node:path";
|
||||||
|
|
||||||
import { Cli, z } from "incur";
|
import { Cli, z } from "incur";
|
||||||
|
import {
|
||||||
|
localPathStringSchema,
|
||||||
|
nulFreeStringSchema,
|
||||||
|
} from "../domain/localPath.js";
|
||||||
|
|
||||||
import { logCliCommand } from "../cliLogging.js";
|
import { logCliCommand } from "../cliLogging.js";
|
||||||
import {
|
import {
|
||||||
@@ -37,7 +42,6 @@ import {
|
|||||||
evidenceBundleOption,
|
evidenceBundleOption,
|
||||||
runEvidenceInputCommand,
|
runEvidenceInputCommand,
|
||||||
} from "./evidenceInputCommand.js";
|
} from "./evidenceInputCommand.js";
|
||||||
import { localPathStringSchema } from "../domain/localPath.js";
|
|
||||||
|
|
||||||
/** Register CLI equivalents of the Electron MCP tools. */
|
/** Register CLI equivalents of the Electron MCP tools. */
|
||||||
export const registerElectronCommands = (
|
export const registerElectronCommands = (
|
||||||
@@ -65,11 +69,9 @@ const registerElectronFuseCommand = (
|
|||||||
description:
|
description:
|
||||||
"Inspect packaged Electron fuse bytes without launching the application",
|
"Inspect packaged Electron fuse bytes without launching the application",
|
||||||
args: z.object({
|
args: z.object({
|
||||||
path: z
|
path: nulFreeStringSchema.describe(
|
||||||
.string()
|
"Local Electron .app, Electron Framework binary, PE executable, or ELF executable",
|
||||||
.describe(
|
),
|
||||||
"Local Electron .app, Electron Framework binary, PE executable, or ELF executable",
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
run: ({ args }) =>
|
run: ({ args }) =>
|
||||||
withCommandCancellation((signal) =>
|
withCommandCancellation((signal) =>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { fridaCapturePolicyInputSchema } from "../domain/fridaCapturePolicy.js";
|
import { fridaCapturePolicyInputSchema } from "../domain/fridaCapturePolicy.js";
|
||||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||||
import { z } from "incur";
|
import { z } from "incur";
|
||||||
|
import { localPathStringSchema } from "../domain/localPath.js";
|
||||||
|
|
||||||
import { FridaInstrumentationService } from "../application/frida/FridaInstrumentationService.js";
|
import { FridaInstrumentationService } from "../application/frida/FridaInstrumentationService.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -166,9 +167,7 @@ export const registerFridaCommands = (
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(`Inline JavaScript to load. ${exactlyOneScriptSource}`),
|
.describe(`Inline JavaScript to load. ${exactlyOneScriptSource}`),
|
||||||
scriptFile: z
|
scriptFile: localPathStringSchema
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
`Path to JavaScript source file. ${exactlyOneScriptSource}`,
|
`Path to JavaScript source file. ${exactlyOneScriptSource}`,
|
||||||
|
|||||||
+43
-30
@@ -27,36 +27,49 @@ export const registerIl2cppCommands = (
|
|||||||
"Local global-metadata.dat (version 24, 27, 29, 31 or 39)",
|
"Local global-metadata.dat (version 24, 27, 29, 31 or 39)",
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
options: z.object({
|
options: z
|
||||||
...analysisEvidenceDeliveryOptions,
|
.object({
|
||||||
binary: localPathStringSchema
|
...analysisEvidenceDeliveryOptions,
|
||||||
.optional()
|
binary: localPathStringSchema
|
||||||
.describe(
|
.optional()
|
||||||
"Matching native binary (Mach-O GameAssembly.dylib, PE GameAssembly.dll or ELF libil2cpp.so) to link method addresses; read only",
|
.describe(
|
||||||
),
|
"Matching native binary (Mach-O GameAssembly.dylib, PE GameAssembly.dll or ELF libil2cpp.so) to link method addresses; read only",
|
||||||
architecture: z
|
),
|
||||||
.enum(["arm64", "amd64"])
|
architecture: z
|
||||||
.optional()
|
.enum(["arm64", "amd64"])
|
||||||
.describe("Slice of a universal Mach-O binary"),
|
.optional()
|
||||||
genericInstances: z
|
.describe("Slice of a universal Mach-O binary"),
|
||||||
.boolean()
|
genericInstances: z
|
||||||
.optional()
|
.boolean()
|
||||||
.describe(
|
.optional()
|
||||||
"Also link generic method instances (Type<Args>$$Method<Args>) and include them in --ghidra-seeds; needs --binary",
|
.describe(
|
||||||
),
|
"Also link generic method instances (Type<Args>$$Method<Args>) and include them in --ghidra-seeds; needs --binary",
|
||||||
ghidraSeeds: localPathStringSchema
|
),
|
||||||
.optional()
|
ghidraSeeds: localPathStringSchema
|
||||||
.describe(
|
.optional()
|
||||||
"Also write REA_GHIDRA_SEED_FILE function seeds named Type$$Method for every linked method; needs --binary",
|
.describe(
|
||||||
),
|
"Also write REA_GHIDRA_SEED_FILE function seeds named Type$$Method for every linked method; needs --binary",
|
||||||
seedBase: z
|
),
|
||||||
.string()
|
seedBase: z
|
||||||
.regex(/^0x[0-9a-fA-F]{1,16}$/u)
|
.string()
|
||||||
.optional()
|
.regex(/^0x[0-9a-fA-F]{1,16}$/u)
|
||||||
.describe(
|
.optional()
|
||||||
"Address Ghidra loads the binary at; seeds are seed-base + RVA (default: Ghidra's loader default, 0x100000 for an ELF linked at 0, else the link base)",
|
.describe(
|
||||||
),
|
"Address Ghidra loads the binary at; seeds are seed-base + RVA (default: Ghidra's loader default, 0x100000 for an ELF linked at 0, else the link base)",
|
||||||
}),
|
),
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
dependentRequired: {
|
||||||
|
architecture: ["binary"],
|
||||||
|
ghidraSeeds: ["binary"],
|
||||||
|
seedBase: ["ghidraSeeds"],
|
||||||
|
},
|
||||||
|
if: {
|
||||||
|
properties: { genericInstances: { const: true } },
|
||||||
|
required: ["genericInstances"],
|
||||||
|
},
|
||||||
|
then: { required: ["binary"] },
|
||||||
|
}),
|
||||||
run: ({ args, options }) =>
|
run: ({ args, options }) =>
|
||||||
withCommandCancellation((signal) =>
|
withCommandCancellation((signal) =>
|
||||||
logCliCommand(logger, CLI_COMMANDS.inspectIl2cppMetadata, async () => {
|
logCliCommand(logger, CLI_COMMANDS.inspectIl2cppMetadata, async () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||||
import type { CliInstance } from "./types.js";
|
import type { CliInstance } from "./types.js";
|
||||||
import { parseNativeMatchingAllowance } from "../config/nativeMatchingAllowance.js";
|
import { parseNativeMatchingAllowance } from "../config/nativeMatchingAllowance.js";
|
||||||
|
import { localPathStringSchema } from "../domain/localPath.js";
|
||||||
|
|
||||||
/** Resolve portable input bundles, then use the same read-only matching service as MCP. */
|
/** Resolve portable input bundles, then use the same read-only matching service as MCP. */
|
||||||
export const registerNativeFunctionMatchingCommands = (
|
export const registerNativeFunctionMatchingCommands = (
|
||||||
@@ -37,14 +38,12 @@ export const registerNativeFunctionMatchingCommands = (
|
|||||||
}),
|
}),
|
||||||
options: z.object({
|
options: z.object({
|
||||||
source_bundle: z
|
source_bundle: z
|
||||||
.array(z.string())
|
.array(localPathStringSchema)
|
||||||
.default([])
|
.default([])
|
||||||
.describe(
|
.describe(
|
||||||
"Canonical source Evidence bundle for retained references; repeat for each input",
|
"Canonical source Evidence bundle for retained references; repeat for each input",
|
||||||
),
|
),
|
||||||
evidence_output: z
|
evidence_output: localPathStringSchema
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Save full matching Evidence and selected sources; required for default summary; never overwrites",
|
"Save full matching Evidence and selected sources; required for default summary; never overwrites",
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ const withOmittableDefaults = (schema: z.ZodObject): z.ZodObject => {
|
|||||||
fillsOmission(field),
|
fillsOmission(field),
|
||||||
);
|
);
|
||||||
if (omittable.length === 0) return schema;
|
if (omittable.length === 0) return schema;
|
||||||
return schema.safeExtend(
|
const metadata = schema.meta();
|
||||||
|
const result = schema.safeExtend(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
omittable.map(([key, field]) => [key, optionalWithMetadata(field)]),
|
omittable.map(([key, field]) => [key, optionalWithMetadata(field)]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return metadata === undefined ? result : result.meta(metadata);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Let the object parser decide omission; Incur advertises required outputs. */
|
/** Let the object parser decide omission; Incur advertises required outputs. */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||||
import { z } from "incur";
|
import { z } from "incur";
|
||||||
|
import { localPathStringSchema } from "../domain/localPath.js";
|
||||||
import { PcapAnalysisService } from "../application/pcap/PcapAnalysisService.js";
|
import { PcapAnalysisService } from "../application/pcap/PcapAnalysisService.js";
|
||||||
import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js";
|
import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js";
|
||||||
import { createPcapAnalysisProvider } from "../composition/pcap.js";
|
import { createPcapAnalysisProvider } from "../composition/pcap.js";
|
||||||
@@ -53,10 +54,9 @@ export const registerPcapCommands = (
|
|||||||
description:
|
description:
|
||||||
"Inspect one pcap/pcapng capture: endpoints, DNS, TLS SNI, and protocol distribution",
|
"Inspect one pcap/pcapng capture: endpoints, DNS, TLS SNI, and protocol distribution",
|
||||||
args: z.object({
|
args: z.object({
|
||||||
path: z
|
path: localPathStringSchema.describe(
|
||||||
.string()
|
"Local pcap or pcapng capture file to inspect",
|
||||||
.min(1)
|
),
|
||||||
.describe("Local pcap or pcapng capture file to inspect"),
|
|
||||||
}),
|
}),
|
||||||
run: ({ args }) =>
|
run: ({ args }) =>
|
||||||
execute(
|
execute(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "incur";
|
import { z } from "incur";
|
||||||
|
import { localPathStringSchema } from "../domain/localPath.js";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js";
|
import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js";
|
||||||
import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js";
|
import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js";
|
||||||
@@ -151,10 +152,9 @@ export const registerReverseEngineeringCommands = (
|
|||||||
description:
|
description:
|
||||||
"Inspect a local artifact using GNU-style objdump arguments and retain the selected command output as Evidence",
|
"Inspect a local artifact using GNU-style objdump arguments and retain the selected command output as Evidence",
|
||||||
args: z.object({
|
args: z.object({
|
||||||
path: z
|
path: localPathStringSchema.describe(
|
||||||
.string()
|
"Path to the local artifact to inspect.",
|
||||||
.min(1)
|
),
|
||||||
.describe("Path to the local artifact to inspect."),
|
|
||||||
operation: z
|
operation: z
|
||||||
.enum(OBJDUMP_ARTIFACT_OPERATIONS)
|
.enum(OBJDUMP_ARTIFACT_OPERATIONS)
|
||||||
.describe("GNU-style objdump operation to run."),
|
.describe("GNU-style objdump operation to run."),
|
||||||
@@ -274,10 +274,9 @@ export const registerReverseEngineeringCommands = (
|
|||||||
cli.command(CLI_COMMANDS.executeRizinCommand, {
|
cli.command(CLI_COMMANDS.executeRizinCommand, {
|
||||||
description: "Execute one Rizin command against a local artifact",
|
description: "Execute one Rizin command against a local artifact",
|
||||||
args: z.object({
|
args: z.object({
|
||||||
path: z
|
path: localPathStringSchema.describe(
|
||||||
.string()
|
"Path to the local artifact to analyze.",
|
||||||
.min(1)
|
),
|
||||||
.describe("Path to the local artifact to analyze."),
|
|
||||||
command: z.string().min(1).describe("One caller-selected Rizin command."),
|
command: z.string().min(1).describe("One caller-selected Rizin command."),
|
||||||
}),
|
}),
|
||||||
options: z.object({
|
options: z.object({
|
||||||
@@ -407,7 +406,9 @@ export const registerReverseEngineeringCommands = (
|
|||||||
description:
|
description:
|
||||||
"Run one unrestricted Rizin debugger command in an ephemeral session",
|
"Run one unrestricted Rizin debugger command in an ephemeral session",
|
||||||
args: z.object({
|
args: z.object({
|
||||||
path: z.string().min(1).describe("Path to the local artifact to debug."),
|
path: localPathStringSchema.describe(
|
||||||
|
"Path to the local artifact to debug.",
|
||||||
|
),
|
||||||
command: z
|
command: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ export const registerSignalCommands = (
|
|||||||
path: ["all"],
|
path: ["all"],
|
||||||
message: "all and limit are mutually exclusive",
|
message: "all and limit are mutually exclusive",
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
not: {
|
||||||
|
required: ["all", "limit"],
|
||||||
|
properties: { all: { const: true } },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
run: ({ options: { all, producerLimits, ...options } }) =>
|
run: ({ options: { all, producerLimits, ...options } }) =>
|
||||||
withCommandCancellation((signal) =>
|
withCommandCancellation((signal) =>
|
||||||
@@ -121,7 +127,13 @@ export const registerSignalCommands = (
|
|||||||
.refine(
|
.refine(
|
||||||
withinSignalRecordingSelectorBudget,
|
withinSignalRecordingSelectorBudget,
|
||||||
signalRecordingSelectorBudgetIssue,
|
signalRecordingSelectorBudgetIssue,
|
||||||
),
|
)
|
||||||
|
.meta({
|
||||||
|
not: {
|
||||||
|
required: ["all_lines", "line_limit"],
|
||||||
|
properties: { all_lines: { const: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
run: ({ args, options: { all_lines, producerLimits, ...options } }) =>
|
run: ({ args, options: { all_lines, producerLimits, ...options } }) =>
|
||||||
withCommandCancellation((signal) =>
|
withCommandCancellation((signal) =>
|
||||||
logCliCommand(logger, CLI_COMMANDS.decodeSignalRecording, async () => {
|
logCliCommand(logger, CLI_COMMANDS.decodeSignalRecording, async () => {
|
||||||
|
|||||||
+38
-42
@@ -347,29 +347,45 @@ const registerReferenceSourceCommand = (
|
|||||||
cli.command(CLI_COMMANDS.importReferenceSource, {
|
cli.command(CLI_COMMANDS.importReferenceSource, {
|
||||||
description: "Import a source tree as historical reference only",
|
description: "Import a source tree as historical reference only",
|
||||||
args: z.object({
|
args: z.object({
|
||||||
root: z
|
root: localPathStringSchema.describe(
|
||||||
.string()
|
"Local source directory to import as historical reference",
|
||||||
.describe("Local source directory to import as historical reference"),
|
),
|
||||||
}),
|
|
||||||
options: z.object({
|
|
||||||
excludePaths: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
"JSON array of Git-ignore rules to exclude. Rules use Git-ignore syntax, including wildcards, directory suffixes, and negations; they are not literal path matches. An empty array adds no caller exclusions.",
|
|
||||||
),
|
|
||||||
detail: z
|
|
||||||
.enum(["full", "summary"])
|
|
||||||
.default("full")
|
|
||||||
.describe(
|
|
||||||
"Complete committed graph or compact historical-source summary",
|
|
||||||
),
|
|
||||||
outlineDepth: cliIntegerArgument(0)
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
"Summary outline path depth (default: 2; 0 omits entries); requires --detail summary",
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
|
options: z
|
||||||
|
.object({
|
||||||
|
excludePaths: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"JSON array of Git-ignore rules to exclude. Rules use Git-ignore syntax, including wildcards, directory suffixes, and negations; they are not literal path matches. An empty array adds no caller exclusions.",
|
||||||
|
),
|
||||||
|
detail: z
|
||||||
|
.enum(["full", "summary"])
|
||||||
|
.default("full")
|
||||||
|
.describe(
|
||||||
|
"Complete committed graph or compact historical-source summary",
|
||||||
|
),
|
||||||
|
outlineDepth: cliIntegerArgument(0)
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Summary outline path depth (default: 2; 0 omits entries); requires --detail summary",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.superRefine((options, context) => {
|
||||||
|
if (options.outlineDepth !== undefined && options.detail !== "summary")
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["outlineDepth"],
|
||||||
|
message: "--outline-depth requires --detail summary",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
if: { required: ["outlineDepth"] },
|
||||||
|
then: {
|
||||||
|
required: ["detail"],
|
||||||
|
properties: { detail: { const: "summary" } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
run: ({ args, options }) =>
|
run: ({ args, options }) =>
|
||||||
logCliCommand(
|
logCliCommand(
|
||||||
logger,
|
logger,
|
||||||
@@ -379,26 +395,6 @@ const registerReferenceSourceCommand = (
|
|||||||
options.excludePaths,
|
options.excludePaths,
|
||||||
);
|
);
|
||||||
if (!excludePaths.ok) return excludePaths.failure;
|
if (!excludePaths.ok) return excludePaths.failure;
|
||||||
if (
|
|
||||||
options.outlineDepth !== undefined &&
|
|
||||||
options.detail !== "summary"
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
error: "Import failed",
|
|
||||||
...projectAnalysisError(
|
|
||||||
new AnalysisInputError(
|
|
||||||
"import-reference-source",
|
|
||||||
{ userMessage: "--outline-depth requires --detail summary" },
|
|
||||||
[
|
|
||||||
{
|
|
||||||
path: ["outlineDepth"],
|
|
||||||
reason: "invalid_value",
|
|
||||||
message: "--outline-depth requires --detail summary",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
const config = parseConfig(environment);
|
const config = parseConfig(environment);
|
||||||
if (!config.ok)
|
if (!config.ok)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -68,3 +68,321 @@ it.each(["draft-2020-12", "draft-07"] as const)(
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const referenceWideningCases: readonly {
|
||||||
|
name: string;
|
||||||
|
schema: Record<string, unknown>;
|
||||||
|
input: unknown;
|
||||||
|
unchanged?: boolean;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: "oneOf",
|
||||||
|
schema: {
|
||||||
|
definitions: { off: { const: false } },
|
||||||
|
properties: { value: { oneOf: [{ $ref: "#/definitions/off" }, {}] } },
|
||||||
|
},
|
||||||
|
input: { value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not",
|
||||||
|
schema: {
|
||||||
|
definitions: { off: { const: false } },
|
||||||
|
properties: { value: { not: { $ref: "#/definitions/off" } } },
|
||||||
|
},
|
||||||
|
input: { value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "if",
|
||||||
|
schema: {
|
||||||
|
definitions: { off: { const: false } },
|
||||||
|
properties: {
|
||||||
|
value: { if: { $ref: "#/definitions/off" }, then: false, else: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: { value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "chain",
|
||||||
|
schema: {
|
||||||
|
definitions: {
|
||||||
|
off: { const: false },
|
||||||
|
alias: { $ref: "#/definitions/off" },
|
||||||
|
},
|
||||||
|
properties: { value: { not: { $ref: "#/definitions/alias" } } },
|
||||||
|
},
|
||||||
|
input: { value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recursive affected",
|
||||||
|
schema: {
|
||||||
|
definitions: {
|
||||||
|
node: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
flag: { const: false },
|
||||||
|
next: { $ref: "#/definitions/node" },
|
||||||
|
},
|
||||||
|
required: ["flag"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: { value: { not: { $ref: "#/definitions/node" } } },
|
||||||
|
},
|
||||||
|
input: { value: { flag: true } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "literal target unchanged",
|
||||||
|
schema: {
|
||||||
|
default: { predicate: { const: false } },
|
||||||
|
properties: { value: { not: { $ref: "#/default/predicate" } } },
|
||||||
|
},
|
||||||
|
input: { value: true },
|
||||||
|
unchanged: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested resource unaffected",
|
||||||
|
schema: {
|
||||||
|
$id: "https://example.test/root",
|
||||||
|
definitions: { flag: { const: false } },
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
$id: "child",
|
||||||
|
definitions: { flag: { const: 7 } },
|
||||||
|
not: { $ref: "#/definitions/flag" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: { value: 8 },
|
||||||
|
unchanged: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recursive unaffected",
|
||||||
|
schema: {
|
||||||
|
definitions: {
|
||||||
|
node: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
flag: { const: 7 },
|
||||||
|
next: { $ref: "#/definitions/node" },
|
||||||
|
},
|
||||||
|
required: ["flag"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: { value: { not: { $ref: "#/definitions/node" } } },
|
||||||
|
},
|
||||||
|
input: { value: { flag: 8 } },
|
||||||
|
unchanged: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(
|
||||||
|
referenceWideningCases.flatMap((entry) =>
|
||||||
|
(["draft-2020-12", "draft-07"] as const).map((dialect) => ({
|
||||||
|
...entry,
|
||||||
|
dialect,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)(
|
||||||
|
"preserves $name through scoped normalization references in $dialect",
|
||||||
|
({ name, schema, input, unchanged, dialect }) => {
|
||||||
|
const canonical: Record<string, unknown> = {
|
||||||
|
$schema:
|
||||||
|
dialect === "draft-07"
|
||||||
|
? "http://json-schema.org/draft-07/schema#"
|
||||||
|
: "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
type: "object",
|
||||||
|
...schema,
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
const create = () =>
|
||||||
|
dialect === "draft-07"
|
||||||
|
? new Ajv({ strict: false })
|
||||||
|
: new Ajv2020({ strict: false });
|
||||||
|
expect(create().compile(canonical)(input), `${name} canonical`).toBe(true);
|
||||||
|
expect(create().compile(advertised)(input), `${name} advertised`).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(advertised.default, `${name} literal data`).toEqual(
|
||||||
|
canonical.default,
|
||||||
|
);
|
||||||
|
if (unchanged)
|
||||||
|
expect(advertised.properties, name).toEqual(canonical.properties);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("keeps an upper contains bound from rejecting canonical items after referenced literal widening", () => {
|
||||||
|
const canonical = {
|
||||||
|
type: "object",
|
||||||
|
definitions: { off: { const: false } },
|
||||||
|
properties: {
|
||||||
|
values: {
|
||||||
|
type: "array",
|
||||||
|
contains: { $ref: "#/definitions/off" },
|
||||||
|
minContains: 0,
|
||||||
|
maxContains: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
const input = { values: [false, true] };
|
||||||
|
expect(new Ajv2020({ strict: false }).compile(canonical)(input)).toBe(true);
|
||||||
|
expect(new Ajv2020({ strict: false }).compile(advertised)(input)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([false, true])(
|
||||||
|
"preserves conditional evaluation annotations (referenced: %s)",
|
||||||
|
(referenced) => {
|
||||||
|
const conditional = {
|
||||||
|
if: { properties: { flag: { const: false } } },
|
||||||
|
then: { properties: { extra: { type: "string" } } },
|
||||||
|
};
|
||||||
|
const canonical = {
|
||||||
|
type: "object",
|
||||||
|
definitions: { conditional },
|
||||||
|
...(referenced ? { $ref: "#/definitions/conditional" } : conditional),
|
||||||
|
unevaluatedProperties: false,
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
const input = { flag: false, extra: "kept" };
|
||||||
|
expect(new Ajv2020({ strict: false }).compile(canonical)(input)).toBe(true);
|
||||||
|
expect(new Ajv2020({ strict: false }).compile(advertised)(input)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("retains item annotations when only the unsafe contains maximum leaves", () => {
|
||||||
|
const canonical = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
values: {
|
||||||
|
type: "array",
|
||||||
|
contains: { const: false },
|
||||||
|
maxContains: 1,
|
||||||
|
unevaluatedItems: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
const input = { values: [false] };
|
||||||
|
expect(new Ajv2020({ strict: false }).compile(canonical)(input)).toBe(true);
|
||||||
|
expect(new Ajv2020({ strict: false }).compile(advertised)(input)).toBe(true);
|
||||||
|
expect(advertised).toHaveProperty("properties.values.contains", {
|
||||||
|
type: "boolean",
|
||||||
|
});
|
||||||
|
expect(advertised).toHaveProperty(
|
||||||
|
"properties.values.unevaluatedItems",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps unaffected root property annotations strict when a child conditional widens", () => {
|
||||||
|
const canonical = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
child: {
|
||||||
|
if: { const: false },
|
||||||
|
then: { type: "string" },
|
||||||
|
else: { type: "boolean" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unevaluatedProperties: false,
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
const validate = new Ajv2020({ strict: false }).compile(advertised);
|
||||||
|
expect(validate({ child: true })).toBe(true);
|
||||||
|
expect(validate({ child: true, unknown: "reject" })).toBe(false);
|
||||||
|
expect(advertised.unevaluatedProperties).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["draft-07", "draft-2020-12"] as const)(
|
||||||
|
"observes reference sibling semantics in %s",
|
||||||
|
(dialect) => {
|
||||||
|
const canonical = {
|
||||||
|
$schema:
|
||||||
|
dialect === "draft-07"
|
||||||
|
? "http://json-schema.org/draft-07/schema#"
|
||||||
|
: "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
type: "object",
|
||||||
|
definitions: {
|
||||||
|
number: { type: "number" },
|
||||||
|
alias: { $ref: "#/definitions/number", const: false },
|
||||||
|
},
|
||||||
|
properties: { value: { not: { $ref: "#/definitions/alias" } } },
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
const create = () =>
|
||||||
|
dialect === "draft-07"
|
||||||
|
? new Ajv({ strict: false, ignoreKeywordsWithRef: true })
|
||||||
|
: new Ajv2020({ strict: false });
|
||||||
|
// Under the specification's draft7 rules alias is number; in2020 it accepts no value.
|
||||||
|
const input = { value: "kept" };
|
||||||
|
expect(create().compile(canonical)(input)).toBe(true);
|
||||||
|
expect(create().compile(advertised)(input)).toBe(true);
|
||||||
|
if (dialect === "draft-07")
|
||||||
|
expect(advertised.properties).toEqual(canonical.properties);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(["draft-07", "draft-2020-12"] as const)(
|
||||||
|
"reduces only an affected reference to opaque data in %s",
|
||||||
|
(dialect) => {
|
||||||
|
const literal = {
|
||||||
|
conditional: {
|
||||||
|
if: { $ref: "#/definitions/off" },
|
||||||
|
then: false,
|
||||||
|
else: true,
|
||||||
|
},
|
||||||
|
unaffected: { type: "string", minLength: 2 },
|
||||||
|
};
|
||||||
|
const canonical = {
|
||||||
|
$schema:
|
||||||
|
dialect === "draft-07"
|
||||||
|
? "http://json-schema.org/draft-07/schema#"
|
||||||
|
: "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
type: "object",
|
||||||
|
definitions: { off: { const: false } },
|
||||||
|
default: literal,
|
||||||
|
properties: {
|
||||||
|
value: { $ref: "#/default/conditional" },
|
||||||
|
stable: { $ref: "#/default/unaffected" },
|
||||||
|
},
|
||||||
|
unevaluatedProperties: false,
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
const create = () =>
|
||||||
|
dialect === "draft-07"
|
||||||
|
? new Ajv({ strict: false })
|
||||||
|
: new Ajv2020({ strict: false });
|
||||||
|
const input = { value: true, stable: "kept" };
|
||||||
|
expect(create().compile(canonical)(input)).toBe(true);
|
||||||
|
const validate = create().compile(advertised);
|
||||||
|
expect(validate(input)).toBe(true);
|
||||||
|
expect(validate({ value: true, stable: 1 })).toBe(false);
|
||||||
|
expect(advertised.default).toEqual(literal);
|
||||||
|
expect(advertised).toHaveProperty(
|
||||||
|
"properties.stable",
|
||||||
|
canonical.properties.stable,
|
||||||
|
);
|
||||||
|
expect(advertised).toHaveProperty(
|
||||||
|
"properties.value.description",
|
||||||
|
expect.stringContaining("reduced form"),
|
||||||
|
);
|
||||||
|
expect(advertised.unevaluatedProperties).toBe(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("does not treat unused definitions as validation dependencies", () => {
|
||||||
|
const canonical = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
not: { type: "number", definitions: { unused: { const: false } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const advertised = presentInputJsonSchema(canonical, () => undefined);
|
||||||
|
expect(advertised).toHaveProperty("properties.value.not.type", "number");
|
||||||
|
const validate = new Ajv2020({ strict: false }).compile(advertised);
|
||||||
|
expect(validate({ value: "kept" })).toBe(true);
|
||||||
|
expect(validate({ value: 1 })).toBe(false);
|
||||||
|
});
|
||||||
@@ -196,25 +196,194 @@ const preserveSharedDefault = (
|
|||||||
schema.default = defaults[0];
|
schema.default = defaults[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
/** Schema children, excluding definitions when following validation dependencies. */
|
||||||
if (!isObject(value)) return { schema: value, changed: false };
|
const normalizationChildren = (
|
||||||
|
schema: Readonly<Record<string, unknown>>,
|
||||||
|
definitions: boolean,
|
||||||
|
): unknown[] =>
|
||||||
|
Object.entries(schema).flatMap(([key, value]) => {
|
||||||
|
if (schemaMaps.has(key) && isObject(value))
|
||||||
|
return !definitions && (key === "$defs" || key === "definitions")
|
||||||
|
? []
|
||||||
|
: Object.values(value);
|
||||||
|
if (schemaArrays.has(key) && Array.isArray(value)) return value;
|
||||||
|
if (schemaChildren.has(key)) return Array.isArray(value) ? value : [value];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNormalizedLiteral = (schema: Readonly<Record<string, unknown>>) =>
|
||||||
|
typeof schema.const === "boolean" ||
|
||||||
|
schema.const === "" ||
|
||||||
|
(Array.isArray(schema.enum) &&
|
||||||
|
((schema.enum.length > 0 &&
|
||||||
|
schema.enum.every((item) => typeof item === "boolean")) ||
|
||||||
|
schema.enum.includes("")));
|
||||||
|
|
||||||
|
/** One projection owns actual rewrite sites and their scoped validation dependencies. */
|
||||||
|
const normalizationContext = (root: Readonly<Record<string, unknown>>) => {
|
||||||
|
const references = new JsonSchemaReferences(root);
|
||||||
|
type Fact = {
|
||||||
|
changed: boolean;
|
||||||
|
lostEvaluation: boolean;
|
||||||
|
dependents: Set<Fact>;
|
||||||
|
evaluationDependents: Set<Fact>;
|
||||||
|
opaqueReference?: Fact;
|
||||||
|
};
|
||||||
|
const sites = new Map<JsonSchemaScope | undefined, WeakSet<object>>();
|
||||||
|
const pending: { schema: unknown; scope: JsonSchemaScope | undefined }[] = [
|
||||||
|
{ schema: root, scope: references.rootScope },
|
||||||
|
];
|
||||||
|
const entries: {
|
||||||
|
schema: Readonly<Record<string, unknown>>;
|
||||||
|
scope: JsonSchemaScope | undefined;
|
||||||
|
}[] = [];
|
||||||
|
// Literal payloads are not rewritten, even when a reference evaluates them.
|
||||||
|
while (pending.length > 0) {
|
||||||
|
const entry = pending.pop()!;
|
||||||
|
if (!isObject(entry.schema)) continue;
|
||||||
|
const scope = references.enter(entry.schema, entry.scope);
|
||||||
|
let seen = sites.get(scope);
|
||||||
|
if (seen === undefined) sites.set(scope, (seen = new WeakSet()));
|
||||||
|
if (seen.has(entry.schema)) continue;
|
||||||
|
seen.add(entry.schema);
|
||||||
|
entries.push({ schema: entry.schema, scope });
|
||||||
|
for (const schema of normalizationChildren(entry.schema, true))
|
||||||
|
pending.push({ schema, scope });
|
||||||
|
}
|
||||||
|
const indexed = new Map<JsonSchemaScope | undefined, WeakMap<object, Fact>>();
|
||||||
|
const work: {
|
||||||
|
schema: Readonly<Record<string, unknown>>;
|
||||||
|
scope: JsonSchemaScope | undefined;
|
||||||
|
fact: Fact;
|
||||||
|
}[] = [];
|
||||||
|
const changed: Fact[] = [];
|
||||||
|
const factAt = (
|
||||||
|
schema: unknown,
|
||||||
|
inherited: JsonSchemaScope | undefined,
|
||||||
|
): Fact | undefined => {
|
||||||
|
if (!isObject(schema)) return undefined;
|
||||||
|
const scope = references.enter(schema, inherited);
|
||||||
|
let facts = indexed.get(scope);
|
||||||
|
if (facts === undefined) indexed.set(scope, (facts = new WeakMap()));
|
||||||
|
const existing = facts.get(schema);
|
||||||
|
if (existing !== undefined) return existing;
|
||||||
|
const fact: Fact = {
|
||||||
|
changed: false,
|
||||||
|
lostEvaluation: false,
|
||||||
|
dependents: new Set(),
|
||||||
|
evaluationDependents: new Set(),
|
||||||
|
};
|
||||||
|
facts.set(schema, fact);
|
||||||
|
work.push({ schema, scope, fact });
|
||||||
|
return fact;
|
||||||
|
};
|
||||||
|
for (const { schema, scope } of entries) factAt(schema, scope);
|
||||||
|
for (let index = 0; index < work.length; index++) {
|
||||||
|
const { schema, scope, fact } = work[index]!;
|
||||||
|
const ignoresSiblings =
|
||||||
|
scope?.dialect === "draft-07" && typeof schema.$ref === "string";
|
||||||
|
if (
|
||||||
|
!ignoresSiblings &&
|
||||||
|
sites.get(scope)?.has(schema) &&
|
||||||
|
hasNormalizedLiteral(schema)
|
||||||
|
) {
|
||||||
|
fact.changed = true;
|
||||||
|
changed.push(fact);
|
||||||
|
}
|
||||||
|
if (typeof schema.$ref === "string") {
|
||||||
|
const target = references.resolve(scope, schema.$ref);
|
||||||
|
if (target.kind === "target") {
|
||||||
|
const dependency = factAt(target.value, scope);
|
||||||
|
dependency?.dependents.add(fact);
|
||||||
|
dependency?.evaluationDependents.add(fact);
|
||||||
|
if (
|
||||||
|
dependency !== undefined &&
|
||||||
|
isObject(target.value) &&
|
||||||
|
!sites.get(references.enter(target.value, scope))?.has(target.value)
|
||||||
|
)
|
||||||
|
fact.opaqueReference = dependency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ignoresSiblings)
|
||||||
|
for (const child of normalizationChildren(schema, false))
|
||||||
|
factAt(child, scope)?.dependents.add(fact);
|
||||||
|
if (!ignoresSiblings)
|
||||||
|
for (const key of [
|
||||||
|
"allOf",
|
||||||
|
"anyOf",
|
||||||
|
"oneOf",
|
||||||
|
"if",
|
||||||
|
"then",
|
||||||
|
"else",
|
||||||
|
"dependentSchemas",
|
||||||
|
"dependencies",
|
||||||
|
]) {
|
||||||
|
const value = schema[key];
|
||||||
|
const children = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: schemaMaps.has(key) && isObject(value)
|
||||||
|
? Object.values(value)
|
||||||
|
: [value];
|
||||||
|
for (const child of children)
|
||||||
|
factAt(child, scope)?.evaluationDependents.add(fact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let index = 0; index < changed.length; index++)
|
||||||
|
for (const dependent of changed[index]!.dependents)
|
||||||
|
if (!dependent.changed) {
|
||||||
|
dependent.changed = true;
|
||||||
|
changed.push(dependent);
|
||||||
|
}
|
||||||
|
const lost: Fact[] = [];
|
||||||
|
for (const { schema, scope, fact } of work)
|
||||||
|
if (
|
||||||
|
sites.get(scope)?.has(schema) &&
|
||||||
|
(fact.opaqueReference?.changed ||
|
||||||
|
(!(scope?.dialect === "draft-07" && typeof schema.$ref === "string") &&
|
||||||
|
factAt(schema.if, scope)?.changed))
|
||||||
|
) {
|
||||||
|
fact.lostEvaluation = true;
|
||||||
|
lost.push(fact);
|
||||||
|
}
|
||||||
|
for (let index = 0; index < lost.length; index++)
|
||||||
|
for (const dependent of lost[index]!.evaluationDependents)
|
||||||
|
if (!dependent.lostEvaluation) {
|
||||||
|
dependent.lostEvaluation = true;
|
||||||
|
lost.push(dependent);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
references,
|
||||||
|
changed: (schema: object, scope: JsonSchemaScope | undefined) =>
|
||||||
|
indexed.get(scope)?.get(schema)?.changed === true,
|
||||||
|
lostEvaluation: (schema: object, scope: JsonSchemaScope | undefined) =>
|
||||||
|
indexed.get(scope)?.get(schema)?.lostEvaluation === true,
|
||||||
|
reduceReference: (schema: object, scope: JsonSchemaScope | undefined) =>
|
||||||
|
indexed.get(scope)?.get(schema)?.opaqueReference?.changed === true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAdvertisedSchema = (
|
||||||
|
value: unknown,
|
||||||
|
context?: ReturnType<typeof normalizationContext>,
|
||||||
|
inherited?: JsonSchemaScope,
|
||||||
|
): SchemaNormalization => {
|
||||||
|
if (!isObject(value)) return { schema: value, changed: false };
|
||||||
|
const owner = context ?? normalizationContext(value);
|
||||||
|
const scope = owner.references.enter(
|
||||||
|
value,
|
||||||
|
context === undefined ? owner.references.rootScope : inherited,
|
||||||
|
);
|
||||||
|
const normalize = (child: unknown) =>
|
||||||
|
normalizeAdvertisedSchema(child, owner, scope);
|
||||||
const node: Record<string, unknown> = { ...value };
|
const node: Record<string, unknown> = { ...value };
|
||||||
let changed = false;
|
const changed = owner.changed(value, scope);
|
||||||
const normalizeMap = (map: Readonly<Record<string, unknown>>) =>
|
const normalizeMap = (map: Readonly<Record<string, unknown>>) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(map).map(([name, child]) => {
|
Object.entries(map).map(([name, child]) => [
|
||||||
const result = normalizeAdvertisedSchema(child);
|
name,
|
||||||
changed ||= result.changed;
|
normalize(child).schema,
|
||||||
return [name, result.schema];
|
]),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
const normalizeList = (list: readonly unknown[]) =>
|
|
||||||
list.map((child) => {
|
|
||||||
const result = normalizeAdvertisedSchema(child);
|
|
||||||
changed ||= result.changed;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [key, child] of Object.entries(value)) {
|
for (const [key, child] of Object.entries(value)) {
|
||||||
if (schemaMaps.has(key) && isObject(child)) {
|
if (schemaMaps.has(key) && isObject(child)) {
|
||||||
@@ -225,7 +394,7 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
key !== "oneOf" &&
|
key !== "oneOf" &&
|
||||||
Array.isArray(child)
|
Array.isArray(child)
|
||||||
) {
|
) {
|
||||||
node[key] = normalizeList(child).map((result) => result.schema);
|
node[key] = child.map(normalize).map((result) => result.schema);
|
||||||
} else if (
|
} else if (
|
||||||
schemaChildren.has(key) &&
|
schemaChildren.has(key) &&
|
||||||
key !== "not" &&
|
key !== "not" &&
|
||||||
@@ -234,23 +403,20 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
key !== "else"
|
key !== "else"
|
||||||
) {
|
) {
|
||||||
const results = Array.isArray(child)
|
const results = Array.isArray(child)
|
||||||
? normalizeList(child)
|
? child.map(normalize)
|
||||||
: [normalizeAdvertisedSchema(child)];
|
: [normalize(child)];
|
||||||
const nestedChanged = results.some((result) => result.changed);
|
const nestedChanged = results.some((result) => result.changed);
|
||||||
if (
|
if (
|
||||||
key === "contains" &&
|
key === "contains" &&
|
||||||
nestedChanged &&
|
nestedChanged &&
|
||||||
value.maxContains !== undefined
|
value.maxContains !== undefined
|
||||||
) {
|
) {
|
||||||
delete node.contains;
|
|
||||||
delete node.minContains;
|
|
||||||
delete node.maxContains;
|
delete node.maxContains;
|
||||||
changed = true;
|
node.contains = results[0]?.schema;
|
||||||
} else {
|
} else {
|
||||||
node[key] = Array.isArray(child)
|
node[key] = Array.isArray(child)
|
||||||
? results.map((result) => result.schema)
|
? results.map((result) => result.schema)
|
||||||
: results[0]?.schema;
|
: results[0]?.schema;
|
||||||
changed ||= nestedChanged;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,12 +424,10 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
if (typeof node.const === "boolean") {
|
if (typeof node.const === "boolean") {
|
||||||
delete node.const;
|
delete node.const;
|
||||||
if (node.type === undefined) node.type = "boolean";
|
if (node.type === undefined) node.type = "boolean";
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
if (node.const === "") {
|
if (node.const === "") {
|
||||||
delete node.const;
|
delete node.const;
|
||||||
if (node.type === undefined) node.type = "string";
|
if (node.type === undefined) node.type = "string";
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
Array.isArray(node.enum) &&
|
Array.isArray(node.enum) &&
|
||||||
@@ -272,7 +436,6 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
) {
|
) {
|
||||||
delete node.enum;
|
delete node.enum;
|
||||||
if (node.type === undefined) node.type = "boolean";
|
if (node.type === undefined) node.type = "boolean";
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
if (Array.isArray(node.enum) && node.enum.includes("")) {
|
if (Array.isArray(node.enum) && node.enum.includes("")) {
|
||||||
// Removing only the empty member would reject a canonical-valid input.
|
// Removing only the empty member would reject a canonical-valid input.
|
||||||
@@ -284,32 +447,29 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
values.every((item) => typeof item === "string")
|
values.every((item) => typeof item === "string")
|
||||||
)
|
)
|
||||||
node.type = "string";
|
node.type = "string";
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const negated = Object.hasOwn(value, "not")
|
const negated = Object.hasOwn(value, "not")
|
||||||
? normalizeAdvertisedSchema(value.not)
|
? normalize(value.not)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (negated?.changed) {
|
if (negated?.changed) {
|
||||||
// Widening a predicate under `not` narrows the containing schema.
|
// Widening a predicate under `not` narrows the containing schema.
|
||||||
changed = true;
|
|
||||||
delete node.not;
|
delete node.not;
|
||||||
} else if (negated !== undefined) {
|
} else if (negated !== undefined) {
|
||||||
node.not = negated.schema;
|
node.not = negated.schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
const conditional = Object.hasOwn(value, "if")
|
const conditional = Object.hasOwn(value, "if")
|
||||||
? normalizeAdvertisedSchema(value.if)
|
? normalize(value.if)
|
||||||
: undefined;
|
: undefined;
|
||||||
const thenBranch = Object.hasOwn(value, "then")
|
const thenBranch = Object.hasOwn(value, "then")
|
||||||
? normalizeAdvertisedSchema(value.then)
|
? normalize(value.then)
|
||||||
: undefined;
|
: undefined;
|
||||||
const elseBranch = Object.hasOwn(value, "else")
|
const elseBranch = Object.hasOwn(value, "else")
|
||||||
? normalizeAdvertisedSchema(value.else)
|
? normalize(value.else)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (conditional?.changed) {
|
if (conditional?.changed) {
|
||||||
// Changing the predicate can route canonical-valid inputs into a rejecting branch.
|
// Changing the predicate can route canonical-valid inputs into a rejecting branch.
|
||||||
changed = true;
|
|
||||||
delete node.if;
|
delete node.if;
|
||||||
delete node.then;
|
delete node.then;
|
||||||
delete node.else;
|
delete node.else;
|
||||||
@@ -317,18 +477,16 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
if (conditional !== undefined) node.if = conditional.schema;
|
if (conditional !== undefined) node.if = conditional.schema;
|
||||||
if (thenBranch !== undefined) {
|
if (thenBranch !== undefined) {
|
||||||
node.then = thenBranch.schema;
|
node.then = thenBranch.schema;
|
||||||
changed ||= thenBranch.changed;
|
|
||||||
}
|
}
|
||||||
if (elseBranch !== undefined) {
|
if (elseBranch !== undefined) {
|
||||||
node.else = elseBranch.schema;
|
node.else = elseBranch.schema;
|
||||||
changed ||= elseBranch.changed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const combinator of ["anyOf", "oneOf"] as const) {
|
for (const combinator of ["anyOf", "oneOf"] as const) {
|
||||||
const branches = value[combinator];
|
const branches = value[combinator];
|
||||||
if (!Array.isArray(branches)) continue;
|
if (!Array.isArray(branches)) continue;
|
||||||
const normalized = normalizeList(branches);
|
const normalized = branches.map(normalize);
|
||||||
const schemas = normalized.map((result) => result.schema);
|
const schemas = normalized.map((result) => result.schema);
|
||||||
const branchesChanged = normalized.some((result) => result.changed);
|
const branchesChanged = normalized.some((result) => result.changed);
|
||||||
if (combinator === "oneOf" && branchesChanged) {
|
if (combinator === "oneOf" && branchesChanged) {
|
||||||
@@ -337,7 +495,6 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
node.type = "boolean";
|
node.type = "boolean";
|
||||||
preserveSharedDefault(node, schemas);
|
preserveSharedDefault(node, schemas);
|
||||||
delete node.oneOf;
|
delete node.oneOf;
|
||||||
changed = true;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// `anyOf` retains branch guidance without allowing widened branches to
|
// `anyOf` retains branch guidance without allowing widened branches to
|
||||||
@@ -349,7 +506,6 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
} else {
|
} else {
|
||||||
node.anyOf = schemas;
|
node.anyOf = schemas;
|
||||||
}
|
}
|
||||||
changed = true;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -360,13 +516,43 @@ const normalizeAdvertisedSchema = (value: unknown): SchemaNormalization => {
|
|||||||
node.type = "boolean";
|
node.type = "boolean";
|
||||||
preserveSharedDefault(node, schemas);
|
preserveSharedDefault(node, schemas);
|
||||||
delete node[combinator];
|
delete node[combinator];
|
||||||
changed = true;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
node[combinator] = schemas;
|
node[combinator] = schemas;
|
||||||
changed ||= branchesChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (owner.lostEvaluation(value, scope)) {
|
||||||
|
delete node.unevaluatedProperties;
|
||||||
|
delete node.unevaluatedItems;
|
||||||
|
}
|
||||||
|
if (owner.reduceReference(value, scope)) {
|
||||||
|
// The target is also caller literal data. Keep that data intact and relax
|
||||||
|
// this affected reference rather than copy or mutate its executable view.
|
||||||
|
if (scope?.dialect === "draft-07") {
|
||||||
|
for (const key of Object.keys(node))
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
"$id",
|
||||||
|
"$schema",
|
||||||
|
"$anchor",
|
||||||
|
"$defs",
|
||||||
|
"definitions",
|
||||||
|
"description",
|
||||||
|
"title",
|
||||||
|
"default",
|
||||||
|
"examples",
|
||||||
|
"$comment",
|
||||||
|
].includes(key)
|
||||||
|
)
|
||||||
|
delete node[key];
|
||||||
|
} else delete node.$ref;
|
||||||
|
node.description = [
|
||||||
|
node.description,
|
||||||
|
"This referenced input schema is advertised in reduced form; the server validates the complete canonical input.",
|
||||||
|
]
|
||||||
|
.filter((text) => typeof text === "string")
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
return { schema: node, changed };
|
return { schema: node, changed };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { processArgumentSchema } from "../process/processArgument.js";
|
||||||
import { fixedTupleSchema } from "../fixedTupleSchema.js";
|
import { fixedTupleSchema } from "../fixedTupleSchema.js";
|
||||||
import {
|
import {
|
||||||
boundedProducerLimitsSchema,
|
boundedProducerLimitsSchema,
|
||||||
@@ -9,11 +10,7 @@ import {
|
|||||||
localPathStringSchema,
|
localPathStringSchema,
|
||||||
} from "../localPath.js";
|
} from "../localPath.js";
|
||||||
|
|
||||||
const argument = z
|
const argument = processArgumentSchema.min(1).max(8192);
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(8192)
|
|
||||||
.refine((value) => !value.includes("\0"), "NUL is not an argument character");
|
|
||||||
const captureInputShape = z.strictObject({
|
const captureInputShape = z.strictObject({
|
||||||
producer_limits: boundedProducerLimitsSchema,
|
producer_limits: boundedProducerLimitsSchema,
|
||||||
path: localPathStringSchema.describe("Absolute local capture path"),
|
path: localPathStringSchema.describe("Absolute local capture path"),
|
||||||
|
|||||||
@@ -89,34 +89,58 @@ const optionalIndex = z.number().int().min(-1);
|
|||||||
const indexWidth = z.union([z.literal(1), z.literal(2), z.literal(4)]);
|
const indexWidth = z.union([z.literal(1), z.literal(2), z.literal(4)]);
|
||||||
|
|
||||||
/** One explicit local file; detail selects delivery, never decoding coverage. */
|
/** One explicit local file; detail selects delivery, never decoding coverage. */
|
||||||
export const inspectIl2cppMetadataInputSchema = z.strictObject({
|
export const inspectIl2cppMetadataInputSchema = z
|
||||||
path: absoluteLocalPathStringSchema.describe(
|
.strictObject({
|
||||||
"Absolute filesystem path to a Unity IL2CPP global-metadata.dat file",
|
path: absoluteLocalPathStringSchema.describe(
|
||||||
),
|
"Absolute filesystem path to a Unity IL2CPP global-metadata.dat file",
|
||||||
detail: z
|
|
||||||
.enum(["complete", "summary"])
|
|
||||||
.default("summary")
|
|
||||||
.describe(
|
|
||||||
"summary (default) retains complete Evidence and returns identity and collection counts; complete returns every image, type, method, member, parameter and string-literal row inline",
|
|
||||||
),
|
),
|
||||||
binary_path: absoluteLocalPathStringSchema
|
detail: z
|
||||||
.optional()
|
.enum(["complete", "summary"])
|
||||||
.describe(
|
.default("summary")
|
||||||
"Absolute path to the caller-selected matching 64-bit IL2CPP binary (Mach-O GameAssembly.dylib, PE GameAssembly.dll or ELF libil2cpp.so); static reading, never executed",
|
.describe(
|
||||||
),
|
"summary (default) retains complete Evidence and returns identity and collection counts; complete returns every image, type, method, member, parameter and string-literal row inline",
|
||||||
binary_architecture: z
|
),
|
||||||
.enum(["arm64", "amd64"])
|
binary_path: absoluteLocalPathStringSchema
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Slice to read from a universal Mach-O binary_path; required for universal binaries and rejected otherwise",
|
"Absolute path to the caller-selected matching 64-bit IL2CPP binary (Mach-O GameAssembly.dylib, PE GameAssembly.dll or ELF libil2cpp.so); static reading, never executed",
|
||||||
),
|
),
|
||||||
generic_instances: z
|
binary_architecture: z
|
||||||
.boolean()
|
.enum(["arm64", "amd64"])
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Also link generic method instances (genericMethodTable rows) to code addresses with Il2CppDumper-style Type<Args>$$Method<Args> names; needs binary_path",
|
"Slice to read from a universal Mach-O binary_path; required for universal binaries and rejected otherwise",
|
||||||
),
|
),
|
||||||
});
|
generic_instances: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Also link generic method instances (genericMethodTable rows) to code addresses with Il2CppDumper-style Type<Args>$$Method<Args> names; needs binary_path",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.superRefine((input, context) => {
|
||||||
|
if (input.binary_path !== undefined) return;
|
||||||
|
if (input.binary_architecture !== undefined)
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["binary_architecture"],
|
||||||
|
message: "binary_architecture selects a slice of binary_path.",
|
||||||
|
});
|
||||||
|
if (input.generic_instances === true)
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["generic_instances"],
|
||||||
|
message: "generic_instances links generic methods in binary_path.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
dependentRequired: { binary_architecture: ["binary_path"] },
|
||||||
|
if: {
|
||||||
|
properties: { generic_instances: { const: true } },
|
||||||
|
required: ["generic_instances"],
|
||||||
|
},
|
||||||
|
then: { required: ["binary_path"] },
|
||||||
|
});
|
||||||
|
|
||||||
export type IL2cppMetadataSectionName = z.infer<
|
export type IL2cppMetadataSectionName = z.infer<
|
||||||
typeof il2cppMetadataSectionSchema
|
typeof il2cppMetadataSectionSchema
|
||||||
|
|||||||
@@ -64,3 +64,16 @@ it("keeps the Windows absolute-path pattern valid in every client mode", () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects unpaired surrogates while preserving representable Unicode paths", () => {
|
||||||
|
for (const path of ["/tmp/\ud800", "/tmp/\udc00", "/tmp/\ud800x"]) {
|
||||||
|
expect(absoluteLocalPathStringSchema.safeParse(path).success, path).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const path of ["/tmp/😀", "/tmp/�", "/tmp/ spaced name "]) {
|
||||||
|
expect(absoluteLocalPathStringSchema.safeParse(path).success, path).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -16,8 +16,13 @@ export const nulFreeStringSchema = z
|
|||||||
.string()
|
.string()
|
||||||
.regex(/^[^\x00]*$/u, "Local filesystem paths cannot contain NUL");
|
.regex(/^[^\x00]*$/u, "Local filesystem paths cannot contain NUL");
|
||||||
|
|
||||||
/** A selected local filesystem path: nonempty and free of NUL bytes. */
|
/** A selected local filesystem path: nonempty, NUL-free, and representable without replacement. */
|
||||||
export const localPathStringSchema = nulFreeStringSchema.min(1);
|
export const localPathStringSchema = nulFreeStringSchema
|
||||||
|
.min(1)
|
||||||
|
.regex(
|
||||||
|
/^[^\uD800-\uDFFF]*$/u,
|
||||||
|
"Local filesystem paths must contain well-formed Unicode",
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Windows absolute path: rooted, UNC, or drive-qualified. The classes spell
|
* A Windows absolute path: rooted, UNC, or drive-qualified. The classes spell
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { z } from "zod";
|
|||||||
|
|
||||||
/** Selected procedure for function dossier and native API analysis. */
|
/** Selected procedure for function dossier and native API analysis. */
|
||||||
export const nativeProcedureInputSchema = z.strictObject({
|
export const nativeProcedureInputSchema = z.strictObject({
|
||||||
procedure: z.string().describe("A procedure name or address"),
|
procedure: z.string().min(1).describe("A procedure name or address"),
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/** One process argument; NUL cannot be represented by process launch APIs. */
|
||||||
|
export const processArgumentSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^[^\x00]*$/u, "NUL is not an argument character");
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { processArgumentSchema } from "../process/processArgument.js";
|
||||||
import {
|
import {
|
||||||
absoluteLocalPathStringSchema,
|
absoluteLocalPathStringSchema,
|
||||||
localPathStringSchema,
|
localPathStringSchema,
|
||||||
@@ -10,11 +11,7 @@ import {
|
|||||||
} from "../producerResourceLimits.js";
|
} from "../producerResourceLimits.js";
|
||||||
|
|
||||||
// These strings are single sigrok arguments, never shell fragments.
|
// These strings are single sigrok arguments, never shell fragments.
|
||||||
const selection = z
|
const selection = processArgumentSchema.min(1).max(8192);
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(8192)
|
|
||||||
.refine((value) => !value.includes("\0"), "NUL is not an argument character");
|
|
||||||
const signalInputShape = z.strictObject({
|
const signalInputShape = z.strictObject({
|
||||||
producer_limits: boundedProducerLimitsSchema,
|
producer_limits: boundedProducerLimitsSchema,
|
||||||
path: localPathStringSchema.describe("Absolute local recording path"),
|
path: localPathStringSchema.describe("Absolute local recording path"),
|
||||||
|
|||||||
@@ -407,30 +407,6 @@ it("charges long digits to the string budget and keeps large previews bounded",
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reads marshal one-byte string tags as Latin-1 with Unicode identity", () => {
|
|
||||||
for (const tag of [0x61, 0x41, 0x7a, 0x5a]) {
|
|
||||||
const encoded = Buffer.concat([
|
|
||||||
Buffer.from([tag]),
|
|
||||||
tag === 0x61 || tag === 0x41 ? u32(1) : Buffer.from([1]),
|
|
||||||
Buffer.from([0xe9]),
|
|
||||||
]);
|
|
||||||
const sameUnicode = marshalUnicode(Buffer.from("é", "utf8"));
|
|
||||||
const set = Buffer.concat([
|
|
||||||
Buffer.from([0x3c]),
|
|
||||||
u32(2),
|
|
||||||
encoded,
|
|
||||||
sameUnicode,
|
|
||||||
]);
|
|
||||||
const report = root(parse(withConsts([encoded, set])));
|
|
||||||
expect(report.consts[0]).toMatchObject({
|
|
||||||
kind: "str",
|
|
||||||
value: "é",
|
|
||||||
bytes: 1,
|
|
||||||
});
|
|
||||||
expect(report.consts[1]).toEqual({ kind: "set", length: 1 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("decodes marshal strings with CPython surrogatepass rules", () => {
|
it("decodes marshal strings with CPython surrogatepass rules", () => {
|
||||||
const report = parse(
|
const report = parse(
|
||||||
withConsts([marshalUnicode(Buffer.from("efbbbf73eda080efbbbf", "hex"))]),
|
withConsts([marshalUnicode(Buffer.from("efbbbf73eda080efbbbf", "hex"))]),
|
||||||
@@ -1558,36 +1534,6 @@ it("hashes a long TYPE_REF tuple chain without recursing", () => {
|
|||||||
expect(pythonPycSchema.safeParse(report).success).toBe(true);
|
expect(pythonPycSchema.safeParse(report).success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hashes shallow TYPE_REF code chains without using the JavaScript stack", () => {
|
|
||||||
const links = 3000;
|
|
||||||
const codes = Array.from({ length: links }, (_, index) =>
|
|
||||||
marshalCode("py311", {
|
|
||||||
flagged: true,
|
|
||||||
name: "f",
|
|
||||||
consts: index === 0 ? [Buffer.from([0x4e])] : [marshalRef(index - 1)],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const bytes = Buffer.concat([
|
|
||||||
pythonPycHeader(3531),
|
|
||||||
marshalCode("py311", {
|
|
||||||
constsObject: Buffer.concat([
|
|
||||||
Buffer.from([0x28]),
|
|
||||||
u32(links + 1),
|
|
||||||
...codes,
|
|
||||||
Buffer.from([0x3c]),
|
|
||||||
u32(1),
|
|
||||||
marshalRef(links - 1),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
const report = parseWith(bytes, { max_objects: 65_536 });
|
|
||||||
expect(root(report).consts.at(-1)).toEqual({ kind: "set", length: 1 });
|
|
||||||
expect(report.coverage).toMatchObject({
|
|
||||||
status: "complete",
|
|
||||||
code_objects: links + 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects code CPython treats as corrupt instead of complete coverage", () => {
|
it("rejects code CPython treats as corrupt instead of complete coverage", () => {
|
||||||
const file = (
|
const file = (
|
||||||
layout: "py37" | "py38" | "py311",
|
layout: "py37" | "py38" | "py311",
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ class MarshalReader {
|
|||||||
readonly #codeKeys = new WeakMap<CodeNode, string>();
|
readonly #codeKeys = new WeakMap<CodeNode, string>();
|
||||||
readonly #codeIdentities = new WeakMap<CodeNode, number>();
|
readonly #codeIdentities = new WeakMap<CodeNode, number>();
|
||||||
readonly #codeEquality = new WeakMap<CodeNode, CodeEquality>();
|
readonly #codeEquality = new WeakMap<CodeNode, CodeEquality>();
|
||||||
/** Completed Python hashability checks per code object. */
|
/** Python hashability per code object. False while that check is in progress. */
|
||||||
readonly #codeHashable = new WeakMap<CodeNode, boolean>();
|
readonly #codeHashable = new WeakMap<CodeNode, boolean>();
|
||||||
/** Tuples whose items are still being read; CPython cannot hash them yet. */
|
/** Tuples whose items are still being read; CPython cannot hash them yet. */
|
||||||
readonly #readingTuples = new WeakSet<MarshalValue>();
|
readonly #readingTuples = new WeakSet<MarshalValue>();
|
||||||
@@ -436,19 +436,13 @@ class MarshalReader {
|
|||||||
};
|
};
|
||||||
case TYPE_INTERNED:
|
case TYPE_INTERNED:
|
||||||
case TYPE_UNICODE:
|
case TYPE_UNICODE:
|
||||||
return this.string(
|
return this.utf8(this.nonNegativeInt32("Marshal string length"), false);
|
||||||
this.nonNegativeInt32("Marshal string length"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
case TYPE_ASCII:
|
case TYPE_ASCII:
|
||||||
case TYPE_ASCII_INTERNED:
|
case TYPE_ASCII_INTERNED:
|
||||||
return this.string(
|
return this.utf8(this.nonNegativeInt32("Marshal string length"), true);
|
||||||
this.nonNegativeInt32("Marshal string length"),
|
|
||||||
"latin1",
|
|
||||||
);
|
|
||||||
case TYPE_SHORT_ASCII:
|
case TYPE_SHORT_ASCII:
|
||||||
case TYPE_SHORT_ASCII_INTERNED:
|
case TYPE_SHORT_ASCII_INTERNED:
|
||||||
return this.string(this.u8(), "latin1");
|
return this.utf8(this.u8(), true);
|
||||||
case TYPE_TUPLE:
|
case TYPE_TUPLE:
|
||||||
return this.sequenceValue(
|
return this.sequenceValue(
|
||||||
"tuple",
|
"tuple",
|
||||||
@@ -1315,70 +1309,13 @@ class MarshalReader {
|
|||||||
#isCodeHashable(node: CodeNode): boolean {
|
#isCodeHashable(node: CodeNode): boolean {
|
||||||
const known = this.#codeHashable.get(node);
|
const known = this.#codeHashable.get(node);
|
||||||
if (known !== undefined) return known;
|
if (known !== undefined) return known;
|
||||||
const frames: { value: MarshalValue; finish: boolean }[] = [
|
this.#codeHashable.set(node, false);
|
||||||
{ value: { kind: "code", value: node }, finish: false },
|
const material =
|
||||||
];
|
this.#codeEquality.get(node) ??
|
||||||
const gray = new Set<MarshalValue | CodeNode>();
|
malformed("Code object is missing equality material.");
|
||||||
const complete = new Set<MarshalValue>();
|
const hashable = this.#equalityKey(material.constsTuple, "hash") !== null;
|
||||||
const reject = (): false => {
|
this.#codeHashable.set(node, hashable);
|
||||||
this.#codeHashable.set(node, false);
|
return hashable;
|
||||||
return false;
|
|
||||||
};
|
|
||||||
while (frames.length > 0) {
|
|
||||||
this.#takeKeyStep();
|
|
||||||
const frame = frames.pop()!;
|
|
||||||
const value = frame.value;
|
|
||||||
const identity = value.kind === "code" ? value.value : value;
|
|
||||||
if (frame.finish) {
|
|
||||||
gray.delete(identity);
|
|
||||||
if (value.kind === "code") {
|
|
||||||
const material =
|
|
||||||
this.#codeEquality.get(value.value) ??
|
|
||||||
malformed("Code object is missing equality material.");
|
|
||||||
// Child code checks are now complete, so the existing tuple key
|
|
||||||
// walker preserves its equality and expansion budgets without recursion.
|
|
||||||
const hashable =
|
|
||||||
this.#equalityKey(material.constsTuple, "hash") !== null;
|
|
||||||
this.#codeHashable.set(value.value, hashable);
|
|
||||||
if (!hashable) return reject();
|
|
||||||
} else complete.add(value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (value.kind === "code") {
|
|
||||||
const cached = this.#codeHashable.get(value.value);
|
|
||||||
if (cached === false) return reject();
|
|
||||||
if (cached === true) continue;
|
|
||||||
} else {
|
|
||||||
const cached = this.#keyCache.get(value);
|
|
||||||
if (cached === null) return reject();
|
|
||||||
if (cached !== undefined || complete.has(value)) continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
gray.has(identity) ||
|
|
||||||
this.#readingTuples.has(value) ||
|
|
||||||
value.kind === "list" ||
|
|
||||||
value.kind === "set" ||
|
|
||||||
value.kind === "dict" ||
|
|
||||||
value.kind === "null"
|
|
||||||
)
|
|
||||||
return reject();
|
|
||||||
const children =
|
|
||||||
value.kind === "code"
|
|
||||||
? [
|
|
||||||
(
|
|
||||||
this.#codeEquality.get(value.value) ??
|
|
||||||
malformed("Code object is missing equality material.")
|
|
||||||
).constsTuple,
|
|
||||||
]
|
|
||||||
: value.kind === "tuple" || value.kind === "frozenset"
|
|
||||||
? value.items
|
|
||||||
: [];
|
|
||||||
gray.add(identity);
|
|
||||||
frames.push({ value, finish: true });
|
|
||||||
for (let index = children.length - 1; index >= 0; index -= 1)
|
|
||||||
frames.push({ value: children[index]!, finish: false });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#codeIdentity(node: CodeNode): number {
|
#codeIdentity(node: CodeNode): number {
|
||||||
@@ -1555,20 +1492,17 @@ class MarshalReader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
string(length: number, encoding: "latin1" | "utf8"): MarshalValue {
|
utf8(length: number, ascii: boolean): MarshalValue {
|
||||||
const bytes = this.blob(length);
|
const bytes = this.blob(length);
|
||||||
if (encoding === "latin1") {
|
if (ascii) {
|
||||||
// CPython reads the ASCII-tagged forms as one-byte Unicode, even when
|
if (bytes.some((byte) => byte > 0x7f))
|
||||||
// high bytes appear. Identity must agree with the UTF-8 string form.
|
malformed("Marshal ASCII string contains a non-ASCII byte.");
|
||||||
const value = bytes.toString("latin1");
|
|
||||||
return {
|
return {
|
||||||
kind: "str",
|
kind: "str",
|
||||||
value,
|
value: bytes.toString("latin1"),
|
||||||
previewEnd: Math.min(length, PYTHON_PYC_CONST_PREVIEW_LIMIT),
|
previewEnd: Math.min(length, PYTHON_PYC_CONST_PREVIEW_LIMIT),
|
||||||
bytes: length,
|
bytes: length,
|
||||||
raw: bytes.some((byte) => byte > 0x7f)
|
raw: bytes,
|
||||||
? Buffer.from(value, "utf8")
|
|
||||||
: bytes,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const decoded = decodeMarshalUtf8(bytes, PYTHON_PYC_CONST_PREVIEW_LIMIT);
|
const decoded = decodeMarshalUtf8(bytes, PYTHON_PYC_CONST_PREVIEW_LIMIT);
|
||||||
|
|||||||
@@ -189,3 +189,34 @@ cliTest(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cliTest(
|
||||||
|
"validates native operation inputs before opening a target",
|
||||||
|
async ({ cli }) => {
|
||||||
|
for (const [selection, code] of [
|
||||||
|
[
|
||||||
|
["inspect-native-instruction", "missing-target", ""],
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["resolve-native-call-targets", "missing-target", ""],
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
],
|
||||||
|
[["annotate-native-function", "missing-target", ""], "VALIDATION_ERROR"],
|
||||||
|
[
|
||||||
|
["annotate-native-function", "missing-target", "main"],
|
||||||
|
"invalid_request",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["annotate-native-function", "missing-target", "main", "--comment="],
|
||||||
|
"target_unavailable",
|
||||||
|
],
|
||||||
|
] as const) {
|
||||||
|
const result = await cli.run({
|
||||||
|
arguments: [...selection, "--json"],
|
||||||
|
environment: { REA_LOG_LEVEL: "silent" },
|
||||||
|
});
|
||||||
|
expect(result.json, JSON.stringify(selection)).toMatchObject({ code });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -27,9 +27,11 @@ const serve = async (cli: CliInstance, argv: readonly string[]) => {
|
|||||||
.object({
|
.object({
|
||||||
args: z
|
args: z
|
||||||
.object({ properties: z.record(z.string(), z.unknown()) })
|
.object({ properties: z.record(z.string(), z.unknown()) })
|
||||||
|
.passthrough()
|
||||||
.optional(),
|
.optional(),
|
||||||
options: z
|
options: z
|
||||||
.object({ properties: z.record(z.string(), z.unknown()) })
|
.object({ properties: z.record(z.string(), z.unknown()) })
|
||||||
|
.passthrough()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.parse(JSON.parse(stdout));
|
.parse(JSON.parse(stdout));
|
||||||
@@ -163,3 +165,115 @@ it("advertises the domain constraints for android, adb settings, and browser sel
|
|||||||
parser.args.safeParse({ path: "app.apk", className: "" }).success,
|
parser.args.safeParse({ path: "app.apk", className: "" }).success,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("advertises selection dependencies after omittable-default projection", async () => {
|
||||||
|
const cli = createCli({});
|
||||||
|
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||||
|
for (const [command, invalid, valid] of [
|
||||||
|
[
|
||||||
|
"inspect-packet-fields",
|
||||||
|
{ all: true, limit: 1 },
|
||||||
|
{ all: false, limit: 1 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"inspect-signal-decoders",
|
||||||
|
{ all: true, limit: 1 },
|
||||||
|
{ all: false, limit: 1 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"inspect-packet-capture",
|
||||||
|
{ fields: ["ip.src"], all_matches: true, match_limit: 1 },
|
||||||
|
{ fields: ["ip.src"], all_matches: false, match_limit: 1 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"decode-signal-recording",
|
||||||
|
{ decoders: "uart", all_lines: true, line_limit: 1 },
|
||||||
|
{ decoders: "uart", all_lines: false, line_limit: 1 },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"inspect-il2cpp-metadata",
|
||||||
|
{ genericInstances: true },
|
||||||
|
{ genericInstances: false },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"inspect-il2cpp-metadata",
|
||||||
|
{ architecture: "arm64" },
|
||||||
|
{ binary: "lib.dylib", architecture: "arm64" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"inspect-il2cpp-metadata",
|
||||||
|
{ ghidraSeeds: "seeds.txt" },
|
||||||
|
{ binary: "lib.so", ghidraSeeds: "seeds.txt" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"inspect-il2cpp-metadata",
|
||||||
|
{ seedBase: "0x1000" },
|
||||||
|
{ binary: "lib.so", ghidraSeeds: "seeds.txt", seedBase: "0x1000" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"import-reference-source",
|
||||||
|
{ outlineDepth: 2, detail: "full" },
|
||||||
|
{ outlineDepth: 2, detail: "summary" },
|
||||||
|
],
|
||||||
|
] as const) {
|
||||||
|
const schema = (await serve(cli, [command, "--schema", "--json"])).options;
|
||||||
|
expect(schema, command).toBeDefined();
|
||||||
|
const validate = ajv.compile(schema as AnySchema);
|
||||||
|
expect(
|
||||||
|
validate(invalid),
|
||||||
|
`${command} invalid ${JSON.stringify(invalid)}`,
|
||||||
|
).toBe(false);
|
||||||
|
expect(validate(valid), `${command} valid ${JSON.stringify(valid)}`).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid selected paths before inspecting a target", async () => {
|
||||||
|
const cli = createCli({});
|
||||||
|
for (const [args, field, code] of [
|
||||||
|
[
|
||||||
|
["inspect-apple-ips", "missing.ips", "--join-path="],
|
||||||
|
"joinPath",
|
||||||
|
"too_small",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["match-native-functions", "missing.json", "--evidence_output="],
|
||||||
|
"evidence_output",
|
||||||
|
"too_small",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["match-native-functions", "missing.json", "--evidence_output=bad\0path"],
|
||||||
|
"evidence_output",
|
||||||
|
"invalid_format",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"match-native-functions",
|
||||||
|
"missing.json",
|
||||||
|
"--evidence_output=bad\ud800path",
|
||||||
|
],
|
||||||
|
"evidence_output",
|
||||||
|
"invalid_format",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
["match-native-functions", "missing.json", "--source_bundle="],
|
||||||
|
"source_bundle.0",
|
||||||
|
"too_small",
|
||||||
|
],
|
||||||
|
[["import-reference-source", ""], "root", "too_small"],
|
||||||
|
] as const) {
|
||||||
|
let stdout = "";
|
||||||
|
await cli.serve([...args, "--json"], {
|
||||||
|
env: {},
|
||||||
|
exit: () => {},
|
||||||
|
stdout: (text) => {
|
||||||
|
stdout += text;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(JSON.parse(stdout), args.join(" ")).toMatchObject({
|
||||||
|
code: "VALIDATION_ERROR",
|
||||||
|
fieldErrors: [{ path: field, code }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -703,15 +703,27 @@ describe("reference-source outline-depth input schema", () => {
|
|||||||
[String(Number.MAX_SAFE_INTEGER), Number.MAX_SAFE_INTEGER],
|
[String(Number.MAX_SAFE_INTEGER), Number.MAX_SAFE_INTEGER],
|
||||||
] as const;
|
] as const;
|
||||||
for (const [token, expected] of parsedValues) {
|
for (const [token, expected] of parsedValues) {
|
||||||
const parsed = parser?.safeParse({ outlineDepth: token });
|
const parsed = parser?.safeParse({
|
||||||
|
outlineDepth: token,
|
||||||
|
detail: "summary",
|
||||||
|
});
|
||||||
expect(parsed?.success).toBe(true);
|
expect(parsed?.success).toBe(true);
|
||||||
if (parsed === undefined || !parsed.success)
|
if (parsed === undefined || !parsed.success)
|
||||||
throw new Error(`Expected outline-depth token ${token} to parse`);
|
throw new Error(`Expected outline-depth token ${token} to parse`);
|
||||||
expect(parsed.data).toMatchObject({ outlineDepth: expected });
|
expect(parsed.data).toMatchObject({
|
||||||
|
detail: "summary",
|
||||||
|
outlineDepth: expected,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for (const token of ["", "-1", "9007199254740992"]) {
|
for (const token of ["", "-1", "9007199254740992"]) {
|
||||||
expect(parser?.safeParse({ outlineDepth: token }).success).toBe(false);
|
expect(
|
||||||
|
parser?.safeParse({ outlineDepth: token, detail: "summary" }).success,
|
||||||
|
).toBe(false);
|
||||||
}
|
}
|
||||||
|
expect(parser?.safeParse({ outlineDepth: "2" }).success).toBe(false);
|
||||||
|
expect(
|
||||||
|
parser?.safeParse({ outlineDepth: "2", detail: "full" }).success,
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
const ajv = new Ajv2020({ strict: false });
|
const ajv = new Ajv2020({ strict: false });
|
||||||
const validate = ajv.compile({
|
const validate = ajv.compile({
|
||||||
@@ -719,13 +731,18 @@ describe("reference-source outline-depth input schema", () => {
|
|||||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||||
});
|
});
|
||||||
for (const [token] of parsedValues) {
|
for (const [token] of parsedValues) {
|
||||||
const parsed = parser?.safeParse({ outlineDepth: token });
|
const parsed = parser?.safeParse({
|
||||||
|
outlineDepth: token,
|
||||||
|
detail: "summary",
|
||||||
|
});
|
||||||
if (parsed === undefined || !parsed.success)
|
if (parsed === undefined || !parsed.success)
|
||||||
throw new Error(`Expected outline-depth token ${token} to parse`);
|
throw new Error(`Expected outline-depth token ${token} to parse`);
|
||||||
expect(validate(parsed.data)).toBe(true);
|
expect(validate(parsed.data)).toBe(true);
|
||||||
}
|
}
|
||||||
expect(validate({ outlineDepth: -1 })).toBe(false);
|
expect(validate({ outlineDepth: -1 })).toBe(false);
|
||||||
expect(validate({ outlineDepth: 9007199254740992 })).toBe(false);
|
expect(validate({ outlineDepth: 9007199254740992 })).toBe(false);
|
||||||
|
expect(validate({ outlineDepth: 2 })).toBe(false);
|
||||||
|
expect(validate({ outlineDepth: 2, detail: "full" })).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1818,3 +1818,20 @@ it("refuses a declared DOS relocation header that is not present in the file", a
|
|||||||
error: { reason: "truncated DOS MZ load module" },
|
error: { reason: "truncated DOS MZ load module" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not resolve malformed Unicode paths to a replacement-character sibling", async () => {
|
||||||
|
const directory = await createTestTempDirectory("rea-target-unicode-");
|
||||||
|
const actual = join(directory, "target-�");
|
||||||
|
const selected = join(directory, "target-\ud800");
|
||||||
|
await writeFile(actual, pe(0xaa64));
|
||||||
|
const refused = await parseBinaryTarget(selected);
|
||||||
|
expect(refused).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "BinaryTargetError",
|
||||||
|
path: selected,
|
||||||
|
reason: "Local filesystem paths must contain well-formed Unicode",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(await parseBinaryTarget(actual)).toMatchObject({ ok: true });
|
||||||
|
});
|
||||||
@@ -121,6 +121,58 @@ const factories = (
|
|||||||
return { dependencies, session };
|
return { dependencies, session };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
it("rejects malformed one-shot operation inputs before creating a session", async () => {
|
||||||
|
const { dependencies, session } = factories();
|
||||||
|
const create = vi.fn(() => session);
|
||||||
|
const guarded = {
|
||||||
|
...dependencies,
|
||||||
|
createBinarySession: create,
|
||||||
|
createNativeBinarySession: create,
|
||||||
|
createArtifactBinarySession: create,
|
||||||
|
createManagedBinarySession: create,
|
||||||
|
};
|
||||||
|
for (const result of [
|
||||||
|
await runDirectAnalysis(guarded, "/missing", "inspect_native_instruction", {
|
||||||
|
address: "",
|
||||||
|
}),
|
||||||
|
await runDirectAnalysis(guarded, "/missing", "annotate_native_function", {
|
||||||
|
procedure: "main",
|
||||||
|
}),
|
||||||
|
await runDirectAnalysis(guarded, "/missing", "trace_feature", {
|
||||||
|
query: "",
|
||||||
|
}),
|
||||||
|
await runDirectAnalysis(guarded, "/missing", "analyze_function", {
|
||||||
|
procedure: "",
|
||||||
|
}),
|
||||||
|
await runDirectAnalysis(guarded, "/missing", "inspect_native_api", {
|
||||||
|
procedure: "",
|
||||||
|
}),
|
||||||
|
await runProviderAnalysis(guarded, "/missing", "observe_native_calls", {
|
||||||
|
breakpoints: [],
|
||||||
|
}),
|
||||||
|
await runProviderAnalysis(guarded, "/missing", "inspect_asset_catalog", {
|
||||||
|
limit: -1,
|
||||||
|
}),
|
||||||
|
]) {
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok)
|
||||||
|
expect(result.error).toMatchObject({ code: "invalid_request" });
|
||||||
|
}
|
||||||
|
const managed = await runManagedProviderExecution(
|
||||||
|
guarded,
|
||||||
|
"/missing",
|
||||||
|
"inspect_managed_members",
|
||||||
|
undefined,
|
||||||
|
{ type_names: ["Duplicate", "Duplicate"] },
|
||||||
|
);
|
||||||
|
expect(managed.ok).toBe(false);
|
||||||
|
if (!managed.ok)
|
||||||
|
expect(projectAnalysisError(managed.error)).toMatchObject({
|
||||||
|
code: "invalid_request",
|
||||||
|
});
|
||||||
|
expect(create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it.each([false, true])(
|
it.each([false, true])(
|
||||||
"retains actual artifact sources and caller path after final-close cancellation (cleanup fails: %s)",
|
"retains actual artifact sources and caller path after final-close cancellation (cleanup fails: %s)",
|
||||||
async (closeFails) => {
|
async (closeFails) => {
|
||||||
@@ -266,9 +318,9 @@ it.each([false, true])(
|
|||||||
...(closeFails ? { cleanupError } : {}),
|
...(closeFails ? { cleanupError } : {}),
|
||||||
});
|
});
|
||||||
const result = analysisCliErrorEnvelopeSchema.parse(
|
const result = analysisCliErrorEnvelopeSchema.parse(
|
||||||
await runDirectAnalysis(dependencies, callerPath, "read_bytes", {}).then(
|
await runDirectAnalysis(dependencies, callerPath, "read_bytes", {
|
||||||
projectCliAnalysisResult,
|
address: "0x1000",
|
||||||
),
|
}).then(projectCliAnalysisResult),
|
||||||
);
|
);
|
||||||
const observation = parseEvidence(
|
const observation = parseEvidence(
|
||||||
jsonObjectSchema.parse(result.details).partial_observation,
|
jsonObjectSchema.parse(result.details).partial_observation,
|
||||||
@@ -342,7 +394,9 @@ describe("one-shot finalization cancellation", () => {
|
|||||||
async (tool) => {
|
async (tool) => {
|
||||||
const [path] = await createBinarySessionTargets();
|
const [path] = await createBinarySessionTargets();
|
||||||
const parameters =
|
const parameters =
|
||||||
tool === "analyze_function" ? { procedure: "fixture" } : {};
|
tool === "analyze_function"
|
||||||
|
? { procedure: "fixture" }
|
||||||
|
: { address: "0x1000" };
|
||||||
const original = parseEvidence(
|
const original = parseEvidence(
|
||||||
await runDirectAnalysis(
|
await runDirectAnalysis(
|
||||||
factories().dependencies,
|
factories().dependencies,
|
||||||
@@ -393,7 +447,7 @@ describe("one-shot finalization cancellation", () => {
|
|||||||
dependencies,
|
dependencies,
|
||||||
path,
|
path,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
{ address: "0x1000" },
|
||||||
{ signal: controller.signal },
|
{ signal: controller.signal },
|
||||||
).then(projectCliAnalysisResult);
|
).then(projectCliAnalysisResult);
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
@@ -420,7 +474,7 @@ describe("one-shot finalization cancellation", () => {
|
|||||||
dependencies,
|
dependencies,
|
||||||
path,
|
path,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
{ address: "0x1000" },
|
||||||
{ signal: controller.signal },
|
{ signal: controller.signal },
|
||||||
).then(projectCliAnalysisResult);
|
).then(projectCliAnalysisResult);
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
@@ -477,7 +531,7 @@ describe("snapshot publication finalization", () => {
|
|||||||
dependencies,
|
dependencies,
|
||||||
path,
|
path,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
{ address: "0x1000" },
|
||||||
{ signal: controller.signal, snapshotPath },
|
{ signal: controller.signal, snapshotPath },
|
||||||
).then(projectCliAnalysisResult);
|
).then(projectCliAnalysisResult);
|
||||||
const loaded = await readAnalysisSnapshot(snapshotPath);
|
const loaded = await readAnalysisSnapshot(snapshotPath);
|
||||||
@@ -539,7 +593,7 @@ describe("snapshot publication finalization", () => {
|
|||||||
dependencies,
|
dependencies,
|
||||||
path,
|
path,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
{ address: "0x1000" },
|
||||||
{ signal: controller.signal, snapshotPath: failure.path },
|
{ signal: controller.signal, snapshotPath: failure.path },
|
||||||
).then(projectCliAnalysisResult),
|
).then(projectCliAnalysisResult),
|
||||||
);
|
);
|
||||||
@@ -574,9 +628,9 @@ it.each([false, true])(
|
|||||||
...(closeFails ? { cleanupError } : {}),
|
...(closeFails ? { cleanupError } : {}),
|
||||||
});
|
});
|
||||||
const result = analysisCliErrorEnvelopeSchema.parse(
|
const result = analysisCliErrorEnvelopeSchema.parse(
|
||||||
await runDirectAnalysis(dependencies, path, "read_bytes", {}).then(
|
await runDirectAnalysis(dependencies, path, "read_bytes", {
|
||||||
projectCliAnalysisResult,
|
address: "0x1000",
|
||||||
),
|
}).then(projectCliAnalysisResult),
|
||||||
);
|
);
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
code: "cleanup_incomplete",
|
code: "cleanup_incomplete",
|
||||||
@@ -601,12 +655,9 @@ describe("one-shot analysis cleanup outcomes", () => {
|
|||||||
it("retains successful evidence and reports CLI failure when close fails", async () => {
|
it("retains successful evidence and reports CLI failure when close fails", async () => {
|
||||||
const [path] = await createBinarySessionTargets();
|
const [path] = await createBinarySessionTargets();
|
||||||
const { dependencies, session } = factories({ cleanupError });
|
const { dependencies, session } = factories({ cleanupError });
|
||||||
const result = await runDirectAnalysis(
|
const result = await runDirectAnalysis(dependencies, path, "read_bytes", {
|
||||||
dependencies,
|
address: "0x1000",
|
||||||
path,
|
}).then(projectCliAnalysisResult);
|
||||||
"read_bytes",
|
|
||||||
{},
|
|
||||||
).then(projectCliAnalysisResult);
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
error: "Analysis failed",
|
error: "Analysis failed",
|
||||||
code: "cleanup_incomplete",
|
code: "cleanup_incomplete",
|
||||||
@@ -631,12 +682,9 @@ describe("one-shot analysis cleanup outcomes", () => {
|
|||||||
cleanupError,
|
cleanupError,
|
||||||
});
|
});
|
||||||
const primary = projectAnalysisError(primaryError);
|
const primary = projectAnalysisError(primaryError);
|
||||||
const result = await runDirectAnalysis(
|
const result = await runDirectAnalysis(dependencies, path, "read_bytes", {
|
||||||
dependencies,
|
address: "0x1000",
|
||||||
path,
|
}).then(projectCliAnalysisResult);
|
||||||
"read_bytes",
|
|
||||||
{},
|
|
||||||
).then(projectCliAnalysisResult);
|
|
||||||
const callerError = analysisCliErrorEnvelopeSchema.parse(result);
|
const callerError = analysisCliErrorEnvelopeSchema.parse(result);
|
||||||
expect(callerError).toMatchObject({
|
expect(callerError).toMatchObject({
|
||||||
code: "cleanup_incomplete",
|
code: "cleanup_incomplete",
|
||||||
@@ -663,12 +711,9 @@ describe("one-shot analysis cleanup outcomes", () => {
|
|||||||
const { dependencies } = factories({
|
const { dependencies } = factories({
|
||||||
thrownCleanup: new Error("unexpected close rejection"),
|
thrownCleanup: new Error("unexpected close rejection"),
|
||||||
});
|
});
|
||||||
const result = await runDirectAnalysis(
|
const result = await runDirectAnalysis(dependencies, path, "read_bytes", {
|
||||||
dependencies,
|
address: "0x1000",
|
||||||
path,
|
}).then(projectCliAnalysisResult);
|
||||||
"read_bytes",
|
|
||||||
{},
|
|
||||||
).then(projectCliAnalysisResult);
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
code: "cleanup_incomplete",
|
code: "cleanup_incomplete",
|
||||||
details: {
|
details: {
|
||||||
@@ -686,9 +731,9 @@ describe("one-shot analysis cleanup outcomes", () => {
|
|||||||
const thrownExecution = new Error("unexpected execution rejection");
|
const thrownExecution = new Error("unexpected execution rejection");
|
||||||
const { dependencies } = factories({ thrownExecution, cleanupError });
|
const { dependencies } = factories({ thrownExecution, cleanupError });
|
||||||
await expect(
|
await expect(
|
||||||
runDirectAnalysis(dependencies, path, "read_bytes", {}).then(
|
runDirectAnalysis(dependencies, path, "read_bytes", {
|
||||||
projectCliAnalysisResult,
|
address: "0x1000",
|
||||||
),
|
}).then(projectCliAnalysisResult),
|
||||||
).rejects.toMatchObject({
|
).rejects.toMatchObject({
|
||||||
cause: thrownExecution,
|
cause: thrownExecution,
|
||||||
errors: [thrownExecution, cleanupError],
|
errors: [thrownExecution, cleanupError],
|
||||||
@@ -698,12 +743,9 @@ describe("one-shot analysis cleanup outcomes", () => {
|
|||||||
it("keeps successful evidence unchanged after confirmed cleanup", async () => {
|
it("keeps successful evidence unchanged after confirmed cleanup", async () => {
|
||||||
const [path] = await createBinarySessionTargets();
|
const [path] = await createBinarySessionTargets();
|
||||||
const { dependencies, session } = factories();
|
const { dependencies, session } = factories();
|
||||||
const result = await runDirectAnalysis(
|
const result = await runDirectAnalysis(dependencies, path, "read_bytes", {
|
||||||
dependencies,
|
address: "0x1000",
|
||||||
path,
|
}).then(projectCliAnalysisResult);
|
||||||
"read_bytes",
|
|
||||||
{},
|
|
||||||
).then(projectCliAnalysisResult);
|
|
||||||
expect(parseEvidence(result)).toMatchObject({
|
expect(parseEvidence(result)).toMatchObject({
|
||||||
operation: "read_bytes",
|
operation: "read_bytes",
|
||||||
normalized_result: { observation: "retained result" },
|
normalized_result: { observation: "retained result" },
|
||||||
|
|||||||
@@ -65,6 +65,28 @@ const recordingFactories = (
|
|||||||
return { dependencies, sessions, clientsClosed: () => clientsClosed };
|
return { dependencies, sessions, clientsClosed: () => clientsClosed };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Schema-valid read_bytes input so lifecycle tests reach the session. */
|
||||||
|
const admittedReadBytes = { address: "0x1000" } as const;
|
||||||
|
|
||||||
|
/** Schema-valid inputs for the ordinary native session route. */
|
||||||
|
const admittedNativeSessionInput = {
|
||||||
|
observe_native_ui: { pid: 123, window_id: 456 },
|
||||||
|
capture_native_ui_scenario: {
|
||||||
|
pid: 123,
|
||||||
|
window_id: 456,
|
||||||
|
steps: [{ kind: "wait", milliseconds: 100 }],
|
||||||
|
},
|
||||||
|
observe_native_calls: {
|
||||||
|
breakpoints: [
|
||||||
|
{
|
||||||
|
kind: "objc-method",
|
||||||
|
class_name: "NSURLSession",
|
||||||
|
selector: "dataTaskWithRequest:completionHandler:",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
it("requires a durable summary destination before acquiring a managed session", async () => {
|
it("requires a durable summary destination before acquiring a managed session", async () => {
|
||||||
const factories = recordingFactories();
|
const factories = recordingFactories();
|
||||||
const result = await runProviderAnalysis(
|
const result = await runProviderAnalysis(
|
||||||
@@ -125,11 +147,13 @@ describe("one-shot analysis factory boundary", () => {
|
|||||||
const factories = recordingFactories(failure);
|
const factories = recordingFactories(failure);
|
||||||
const interruptListeners = process.listenerCount("SIGINT");
|
const interruptListeners = process.listenerCount("SIGINT");
|
||||||
const terminateListeners = process.listenerCount("SIGTERM");
|
const terminateListeners = process.listenerCount("SIGTERM");
|
||||||
await expect(
|
const execution = runDirectAnalysis(
|
||||||
runDirectAnalysis(factories.dependencies, path, "read_bytes", {}).then(
|
factories.dependencies,
|
||||||
projectCliAnalysisResult,
|
path,
|
||||||
),
|
"read_bytes",
|
||||||
).rejects.toBe(failure);
|
admittedReadBytes,
|
||||||
|
).then(projectCliAnalysisResult);
|
||||||
|
await expect(execution).rejects.toBe(failure);
|
||||||
expect(factories.clientsClosed()).toBe(1);
|
expect(factories.clientsClosed()).toBe(1);
|
||||||
expect(process.listenerCount("SIGINT")).toBe(interruptListeners);
|
expect(process.listenerCount("SIGINT")).toBe(interruptListeners);
|
||||||
expect(process.listenerCount("SIGTERM")).toBe(terminateListeners);
|
expect(process.listenerCount("SIGTERM")).toBe(terminateListeners);
|
||||||
@@ -247,33 +271,33 @@ describe("one-shot analysis factory boundary", () => {
|
|||||||
expect(factories.sessions[0]?.activeTarget()).toBeUndefined();
|
expect(factories.sessions[0]?.activeTarget()).toBeUndefined();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each([
|
|
||||||
"observe_native_ui",
|
|
||||||
"capture_native_ui_scenario",
|
|
||||||
"observe_native_calls",
|
|
||||||
] satisfies readonly NativeToolName[])(
|
|
||||||
"keeps %s on the ordinary session route",
|
|
||||||
async (tool) => {
|
|
||||||
const [path] = await createBinarySessionTargets();
|
|
||||||
const factories = recordingFactories();
|
|
||||||
const result = await runProviderAnalysis(
|
|
||||||
{
|
|
||||||
...factories.dependencies,
|
|
||||||
createNativeBinarySession: () => {
|
|
||||||
throw new Error("static inspection factory must stay unused");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
path,
|
|
||||||
tool,
|
|
||||||
{},
|
|
||||||
).then(projectCliAnalysisResult);
|
|
||||||
expect(result).toMatchObject({ operation: tool });
|
|
||||||
expect(factories.clientsClosed()).toBe(1);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"observe_native_ui",
|
||||||
|
"capture_native_ui_scenario",
|
||||||
|
"observe_native_calls",
|
||||||
|
] satisfies readonly NativeToolName[])(
|
||||||
|
"keeps %s on the ordinary session route",
|
||||||
|
async (tool) => {
|
||||||
|
const [path] = await createBinarySessionTargets();
|
||||||
|
const factories = recordingFactories();
|
||||||
|
const result = await runProviderAnalysis(
|
||||||
|
{
|
||||||
|
...factories.dependencies,
|
||||||
|
createNativeBinarySession: () => {
|
||||||
|
throw new Error("static inspection factory must stay unused");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path,
|
||||||
|
tool,
|
||||||
|
admittedNativeSessionInput[tool],
|
||||||
|
).then(projectCliAnalysisResult);
|
||||||
|
expect(result).toMatchObject({ operation: tool });
|
||||||
|
expect(factories.clientsClosed()).toBe(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("preserves a managed execution profile in direct analysis Evidence", async () => {
|
it("preserves a managed execution profile in direct analysis Evidence", async () => {
|
||||||
const [path] = await createBinarySessionTargets();
|
const [path] = await createBinarySessionTargets();
|
||||||
const profile = createAnalysisProfile(
|
const profile = createAnalysisProfile(
|
||||||
@@ -307,7 +331,7 @@ it("direct analysis snapshot target mismatch retains actionable snapshot recover
|
|||||||
factories.dependencies,
|
factories.dependencies,
|
||||||
path,
|
path,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
admittedReadBytes,
|
||||||
{ snapshotPath },
|
{ snapshotPath },
|
||||||
).then(projectCliAnalysisResult),
|
).then(projectCliAnalysisResult),
|
||||||
).not.toHaveProperty("code");
|
).not.toHaveProperty("code");
|
||||||
@@ -315,7 +339,7 @@ it("direct analysis snapshot target mismatch retains actionable snapshot recover
|
|||||||
factories.dependencies,
|
factories.dependencies,
|
||||||
other,
|
other,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
admittedReadBytes,
|
||||||
{ snapshotPath },
|
{ snapshotPath },
|
||||||
).then(projectCliAnalysisResult);
|
).then(projectCliAnalysisResult);
|
||||||
const sha = async (input: string) =>
|
const sha = async (input: string) =>
|
||||||
@@ -344,7 +368,7 @@ it("direct analysis snapshot target mismatch retains actionable snapshot recover
|
|||||||
factories.dependencies,
|
factories.dependencies,
|
||||||
path,
|
path,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
admittedReadBytes,
|
||||||
{ snapshotPath },
|
{ snapshotPath },
|
||||||
).then(projectCliAnalysisResult),
|
).then(projectCliAnalysisResult),
|
||||||
).not.toHaveProperty("code");
|
).not.toHaveProperty("code");
|
||||||
@@ -385,7 +409,7 @@ it("retries snapshot file cleanup before final close even when a published ident
|
|||||||
dependencies,
|
dependencies,
|
||||||
path,
|
path,
|
||||||
"read_bytes",
|
"read_bytes",
|
||||||
{},
|
admittedReadBytes,
|
||||||
{ snapshotPath: join(dirname(path), "excluded.snapshot") },
|
{ snapshotPath: join(dirname(path), "excluded.snapshot") },
|
||||||
).then(projectCliAnalysisResult);
|
).then(projectCliAnalysisResult);
|
||||||
expect(events[0]).toBe("file-cleanup");
|
expect(events[0]).toBe("file-cleanup");
|
||||||
|
|||||||
@@ -101,12 +101,32 @@ const fixture = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("direct CLI cross-reference selectors", () => {
|
describe("direct CLI cross-reference selectors", () => {
|
||||||
it("retains validation issues unrelated to symbol resolution", async () => {
|
it("rejects unknown arguments before opening a session", async () => {
|
||||||
const [path] = await createBinarySessionTargets();
|
const [path] = await createBinarySessionTargets();
|
||||||
const issues = [
|
const provider = fixture({
|
||||||
{ path: ["address"], reason: "invalid_format" },
|
xrefsError: new AnalysisInputError("xrefs", undefined, [
|
||||||
{ path: ["extra"], reason: "unknown_argument" },
|
{ path: ["address"], reason: "invalid_format" },
|
||||||
] as const;
|
]),
|
||||||
|
});
|
||||||
|
const result = await runDirectAnalysis(
|
||||||
|
provider.dependencies,
|
||||||
|
path,
|
||||||
|
"xrefs",
|
||||||
|
{ address: "table_only", extra: true },
|
||||||
|
).then(projectCliAnalysisResult);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
code: "invalid_request",
|
||||||
|
details: {
|
||||||
|
operation: "xrefs",
|
||||||
|
issues: [{ path: ["extra"], reason: "unknown_argument" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(provider.closed()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains provider validation issues unrelated to symbol resolution", async () => {
|
||||||
|
const [path] = await createBinarySessionTargets();
|
||||||
|
const issues = [{ path: ["address"], reason: "invalid_format" }] as const;
|
||||||
const provider = fixture({
|
const provider = fixture({
|
||||||
xrefsError: new AnalysisInputError("xrefs", undefined, issues),
|
xrefsError: new AnalysisInputError("xrefs", undefined, issues),
|
||||||
});
|
});
|
||||||
@@ -114,7 +134,7 @@ describe("direct CLI cross-reference selectors", () => {
|
|||||||
provider.dependencies,
|
provider.dependencies,
|
||||||
path,
|
path,
|
||||||
"xrefs",
|
"xrefs",
|
||||||
{ address: "table_only", extra: true },
|
{ address: "table_only" },
|
||||||
).then(projectCliAnalysisResult);
|
).then(projectCliAnalysisResult);
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
code: "invalid_request",
|
code: "invalid_request",
|
||||||
|
|||||||
Reference in new issue
Block a user