mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(native): preserve Objective-C records with unreadable protocol lists
Keep independent Objective-C class, metaclass, category and protocol records when an adopted-protocol list is unreadable. The shared list reader retains readable prefixes through tagged Result failures; consumers preserve partial/unknown semantics with unchanged limits. Advance the source-owned reader commitment to v3 and reject stale v2 snapshot bindings. Validated with 232 focused tests, current-main and original-proposal RED checks, named provider-result/schema/identity controls, and a verified public GLFW Mach-O paired with independent LLVM/Python byte evidence. Ordinary binary results remain identical; a deliberately mutated count preserves ten rows instead of eight. The final head passes the real macOS CI lane, with twelve CLI tests including both new protocol-count cases over CLI and stdio MCP. Snapshot binding and same-version replay checks pass. The original unchecked human declarations are preserved. The explicit maintainer readiness exception applies after technical CI and automated review, without adding a human-review attestation. Fixes #8945 Full technical CI 42263548929 passes, including all eight shards and required aggregates. The Author readiness-only exception is explicitly authorized; no human-review attestation is added. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
This commit is contained in:
1 parent
f8356f3d71
commit
81ada025c0
10 files changed
+344
-32
No files matched your search
@@ -927,6 +927,13 @@ complete observation.
|
||||
section lengths and unsupported non-PowerPC big-endian CPU declarations return
|
||||
`unsupported_target` with the selected path and precise reason. Recovery
|
||||
selects a well-formed or supported artifact and reopens that target.
|
||||
An unreadable adopted-protocol sublist leaves its class, category or
|
||||
`objc_protocol_records` entry available with partial decode status and
|
||||
`protocol_list_unreadable`. Independently decoded metadata and any readable
|
||||
protocol-name prefix remain available; an empty partial list does not
|
||||
establish that its owner adopts no protocols. Existing list and record
|
||||
limits remain in effect. Reader v3 uses a new analysis profile; snapshots
|
||||
bound to reader v2 are incompatible and must be regenerated for this recovery.
|
||||
The byte reader holds the whole selected file in memory, so it admits files up
|
||||
to 64 MiB / 67108864 bytes by default. Select a larger budget with
|
||||
`max_input_bytes` (CLI `--max-input-bytes`), at most 1 GiB / 1073741824
|
||||
|
||||
@@ -2478,3 +2478,144 @@ it.each([false, true])(
|
||||
).toBe(unreadParent ? "partial" : "complete");
|
||||
},
|
||||
);
|
||||
|
||||
describe("Objective-C adopted protocol-list count limit", () => {
|
||||
// Root class and metaclass that adopt the fixture protocol through the
|
||||
// class_ro_t base protocol list at 0x700.
|
||||
const rootClass = (protocolCount: bigint, category?: "partial-method") => {
|
||||
const bytes = fixture(false, category);
|
||||
bytes.writeUInt32LE(2, 12);
|
||||
bytes.writeUInt32LE(5, 88);
|
||||
bytes.writeUInt32LE(2, 0x300);
|
||||
bytes.writeUInt32LE(3, 0x380);
|
||||
bytes.writeBigUInt64LE(protocolCount, 0x700);
|
||||
return bytes;
|
||||
};
|
||||
const classCoverage = (
|
||||
result: ReturnType<typeof decodeAppleDispatchMetadata>,
|
||||
) =>
|
||||
result.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
);
|
||||
|
||||
it("retains the class and metaclass rows when the list exceeds its count limit", () => {
|
||||
const result = decodeAppleDispatchMetadata(
|
||||
rootClass(20001n),
|
||||
100,
|
||||
provenance,
|
||||
);
|
||||
expect(
|
||||
result.objc_classes.map(({ name, is_meta_class, protocols, decode }) => ({
|
||||
name,
|
||||
is_meta_class,
|
||||
protocols,
|
||||
decode,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
name: "Fixture",
|
||||
is_meta_class: false,
|
||||
protocols: [],
|
||||
decode: { status: "partial", reason: "protocol_list_unreadable" },
|
||||
},
|
||||
// The metaclass has no adopted-protocol list of its own.
|
||||
{
|
||||
name: "Fixture",
|
||||
is_meta_class: true,
|
||||
protocols: [],
|
||||
decode: { status: "decoded", reason: null },
|
||||
},
|
||||
]);
|
||||
expect(result.objc_classes[0]?.properties).toHaveLength(3);
|
||||
expect(result.objc_ivars).toHaveLength(1);
|
||||
expect(result.objc_dispatch_implementations).toMatchObject([
|
||||
{ class_name: "Fixture", selector: "performAction:" },
|
||||
]);
|
||||
expect(classCoverage(result)).toMatchObject({
|
||||
status: "partial",
|
||||
// The class row is retained as partial, so it is examined but not
|
||||
// counted as fully decoded; the metaclass is fully decoded.
|
||||
examined: 2,
|
||||
decoded: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains the readable protocol prefix when a later list slot is unmapped", () => {
|
||||
const bytes = rootClass(2n);
|
||||
bytes.writeBigUInt64LE(base + 0x5000n, 0x710);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_classes).toMatchObject([
|
||||
{
|
||||
name: "Fixture",
|
||||
protocols: ["FixtureProtocol"],
|
||||
decode: { status: "partial", reason: "protocol_list_unreadable" },
|
||||
},
|
||||
{ name: "Fixture", is_meta_class: true },
|
||||
]);
|
||||
expect(result.objc_protocol_records).toMatchObject([
|
||||
{ name: "FixtureProtocol", decode: { status: "decoded" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains a category's own methods and properties after a protocol-list refusal", () => {
|
||||
const bytes = rootClass(0n, "partial-method");
|
||||
bytes.writeBigUInt64LE(base + 0x700n, 0xb98);
|
||||
bytes.writeBigUInt64LE(base + 0x600n, 0xb68);
|
||||
bytes.writeBigUInt64LE(base + 0xa00n, 0xb60);
|
||||
bytes.writeBigUInt64LE(20001n, 0xa00);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_categories).toMatchObject([
|
||||
{
|
||||
name: "FixtureCategory",
|
||||
class_name: "Fixture",
|
||||
instance_methods: ["performAction:"],
|
||||
protocols: [],
|
||||
decode: { status: "partial", reason: "protocol_list_unreadable" },
|
||||
},
|
||||
]);
|
||||
expect(result.objc_categories[0]?.properties).toHaveLength(3);
|
||||
expect(classCoverage(result)?.status).toBe("complete");
|
||||
expect(
|
||||
result.coverage.find(
|
||||
({ facet }) => facet === "objc_properties_categories",
|
||||
),
|
||||
).toMatchObject({ status: "partial" });
|
||||
});
|
||||
|
||||
it("retains an adopting protocol's own method declarations after a list refusal", () => {
|
||||
const bytes = rootClass(1n);
|
||||
bytes.writeBigUInt64LE(base + 0xa00n, 0x750);
|
||||
bytes.writeBigUInt64LE(20001n, 0xa00);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_protocol_records).toMatchObject([
|
||||
{
|
||||
name: "FixtureProtocol",
|
||||
adopted_protocols: [],
|
||||
methods: [{ selector: "requiredAction:" }],
|
||||
optional_methods: [{ selector: "optionalAction:" }],
|
||||
decode: { status: "partial", reason: "protocol_list_unreadable" },
|
||||
},
|
||||
]);
|
||||
expect(result.objc_protocols).toMatchObject([
|
||||
{ name: "FixtureProtocol", methods: [{ selector: "requiredAction:" }] },
|
||||
]);
|
||||
expect(result.objc_classes).toHaveLength(2);
|
||||
expect(classCoverage(result)?.status).toBe("partial");
|
||||
});
|
||||
|
||||
it.each([0n, 1n])(
|
||||
"keeps complete class coverage for an in-limit count of %s",
|
||||
(count) => {
|
||||
const bytes = rootClass(count);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
const reason = classCoverage(result)?.reason ?? "";
|
||||
expect(reason).not.toContain("Protocol list of Fixture");
|
||||
expect(result.objc_classes).toHaveLength(2);
|
||||
for (const row of result.objc_classes)
|
||||
expect(row.decode?.reason ?? "").not.toContain(
|
||||
"protocol_list_unreadable",
|
||||
);
|
||||
expect(classCoverage(result)?.status).toBe("complete");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import type { ObjcDispatchReaders } from "./AppleObjcDispatchFacets.js";
|
||||
import { readObjcPropertiesOf } from "./AppleObjcProperties.js";
|
||||
import { issue, type DecodeIssue } from "./AppleDispatchDecodeFacts.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
import type { ObjcProtocolListResult } from "./AppleObjcProtocols.js";
|
||||
|
||||
const CLASS_SYMBOL = /^_OBJC_(?:META)?CLASS_\$_(.+)$/u;
|
||||
|
||||
@@ -50,7 +51,11 @@ export const decodeObjcCategories = (input: {
|
||||
category: string,
|
||||
onTruncated?: () => void,
|
||||
) => string[];
|
||||
readonly protocols: (list: bigint, onTruncated?: () => void) => string[];
|
||||
/** Read the protocol-list pointer field, retaining known names on failure. */
|
||||
readonly protocolList: (
|
||||
field: bigint,
|
||||
onTruncated?: () => void,
|
||||
) => ObjcProtocolListResult;
|
||||
/** Name of a class defined in this image, from its class_ro_t. */
|
||||
readonly localClassName: (address: bigint) => string;
|
||||
}): number => {
|
||||
@@ -119,10 +124,16 @@ export const decodeObjcCategories = (input: {
|
||||
name,
|
||||
onTruncated,
|
||||
);
|
||||
const protocols = input.protocols(
|
||||
read.pointer(category + 32n),
|
||||
onTruncated,
|
||||
);
|
||||
const protocols = input.protocolList(category + 32n, onTruncated);
|
||||
if (!protocols.ok)
|
||||
input.issues.push(
|
||||
issue(
|
||||
"objc_properties_categories",
|
||||
"protocol_list_unreadable",
|
||||
`Category ${name} adopted protocol list: ${protocols.error.reason}`,
|
||||
`0x${(category + 32n).toString(16)}`,
|
||||
),
|
||||
);
|
||||
const properties = readObjcPropertiesOf(
|
||||
read,
|
||||
read.pointer(category + 40n),
|
||||
@@ -136,6 +147,7 @@ export const decodeObjcCategories = (input: {
|
||||
);
|
||||
const decodeReasons = [
|
||||
...(className === null ? ["category_class_unresolved"] : []),
|
||||
...(protocols.ok ? [] : ["protocol_list_unreadable"]),
|
||||
...(budgetTruncated ? ["max_records_reached"] : []),
|
||||
];
|
||||
result.objc_categories.push({
|
||||
@@ -149,7 +161,7 @@ export const decodeObjcCategories = (input: {
|
||||
: "local",
|
||||
instance_methods: instanceMethods,
|
||||
class_methods: classMethods,
|
||||
protocols,
|
||||
protocols: protocols.ok ? protocols.value : protocols.error.names,
|
||||
properties,
|
||||
location: read.location(category),
|
||||
decode:
|
||||
|
||||
@@ -361,12 +361,20 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
location: hex(read.pointer(ro + 64n)),
|
||||
onTruncated,
|
||||
});
|
||||
const protocols = protocolReader.list(read.pointer(ro + 40n), onTruncated);
|
||||
const adoptedProtocols = protocolReader.readList(ro + 40n, onTruncated);
|
||||
if (!adoptedProtocols.ok)
|
||||
failures.push(
|
||||
`Protocol list of ${name}: ${adoptedProtocols.error.reason}`,
|
||||
);
|
||||
const protocols = adoptedProtocols.ok
|
||||
? adoptedProtocols.value
|
||||
: adoptedProtocols.error.names;
|
||||
const classDecodeReasons = [
|
||||
...(superclass.name === null && !root
|
||||
? ["superclass_binding_unresolved"]
|
||||
: []),
|
||||
...(classListTruncated ? ["max_records_reached"] : []),
|
||||
...(adoptedProtocols.ok ? [] : ["protocol_list_unreadable"]),
|
||||
];
|
||||
result.objc_classes.push({
|
||||
name,
|
||||
@@ -449,7 +457,7 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
issues: categoryIssues,
|
||||
admit: () => budget.admit(),
|
||||
methods,
|
||||
protocols: (list, onTruncated) => protocolReader.list(list, onTruncated),
|
||||
protocolList: protocolReader.readList,
|
||||
localClassName: (address) => readLocalClass(read, address).name,
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { readObjcPropertyList } from "./AppleObjcProperties.js";
|
||||
import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js";
|
||||
import type { SwiftNominalReaders } from "./AppleSwiftNominalTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
/** A failed list still owns the names read before its unreadable suffix. */
|
||||
export type ObjcProtocolListResult = Result<
|
||||
string[],
|
||||
{
|
||||
readonly _tag: "ProtocolListUnreadable";
|
||||
readonly names: string[];
|
||||
readonly reason: string;
|
||||
}
|
||||
>;
|
||||
|
||||
/** Read protocol lists and absolute protocol method declarations with cycle-safe identities. */
|
||||
export const createObjcProtocolReader = (input: {
|
||||
@@ -19,23 +30,36 @@ export const createObjcProtocolReader = (input: {
|
||||
const { readers: read, result, failures } = input;
|
||||
const visited = new Set<bigint>();
|
||||
const name = (address: bigint) => read.string(read.pointer(address + 8n));
|
||||
const list = (address: bigint, onTruncated?: () => void): string[] => {
|
||||
if (address === 0n) return [];
|
||||
// `count` is plain uintptr_t data, not a pointer fixup.
|
||||
const count = read.u64(address);
|
||||
if (count > 20000n)
|
||||
throw new RangeError("Protocol list exceeds 20000 entries");
|
||||
/** Own the list pointer field as well as the list's count and entries. */
|
||||
const readList = (
|
||||
field: bigint,
|
||||
onTruncated?: () => void,
|
||||
): ObjcProtocolListResult => {
|
||||
const names: string[] = [];
|
||||
for (let index = 0n; index < count; index++) {
|
||||
if (!read.admit()) {
|
||||
onTruncated?.();
|
||||
break;
|
||||
try {
|
||||
const address = read.pointer(field);
|
||||
if (address === 0n) return ok(names);
|
||||
// `count` is plain uintptr_t data, not a pointer fixup.
|
||||
const count = read.u64(address);
|
||||
if (count > 20000n)
|
||||
throw new RangeError("Protocol list exceeds 20000 entries");
|
||||
for (let index = 0n; index < count; index++) {
|
||||
if (!read.admit()) {
|
||||
onTruncated?.();
|
||||
break;
|
||||
}
|
||||
const protocol = read.pointer(address + 8n + index * 8n);
|
||||
names.push(name(protocol));
|
||||
record(protocol);
|
||||
}
|
||||
const protocol = read.pointer(address + 8n + index * 8n);
|
||||
names.push(name(protocol));
|
||||
record(protocol);
|
||||
return ok(names);
|
||||
} catch (cause: unknown) {
|
||||
return err({
|
||||
_tag: "ProtocolListUnreadable",
|
||||
names,
|
||||
reason: cause instanceof Error ? cause.message : String(cause),
|
||||
});
|
||||
}
|
||||
return names;
|
||||
};
|
||||
const methods = (
|
||||
address: bigint,
|
||||
@@ -97,7 +121,11 @@ export const createObjcProtocolReader = (input: {
|
||||
const onTruncated = () => {
|
||||
budgetTruncated = true;
|
||||
};
|
||||
const adopted = list(read.pointer(address + 16n), onTruncated);
|
||||
const adopted = readList(address + 16n, onTruncated);
|
||||
if (!adopted.ok)
|
||||
failures.push(
|
||||
`Protocol 0x${address.toString(16)} adopted protocol list: ${adopted.error.reason}`,
|
||||
);
|
||||
if (protocolName.length === 0)
|
||||
throw new TypeError("Objective-C protocol has an empty name");
|
||||
const skipped: string[] = [];
|
||||
@@ -165,6 +193,7 @@ export const createObjcProtocolReader = (input: {
|
||||
);
|
||||
const decodeReasons = [
|
||||
...propertyReasons,
|
||||
...(adopted.ok ? [] : ["protocol_list_unreadable"]),
|
||||
...(skipped.length === 0 ? [] : ["protocol_method_selector_empty"]),
|
||||
...(budgetTruncated ? ["max_records_reached"] : []),
|
||||
];
|
||||
@@ -176,7 +205,7 @@ export const createObjcProtocolReader = (input: {
|
||||
});
|
||||
result.objc_protocol_records.push({
|
||||
name: protocolName,
|
||||
adopted_protocols: adopted,
|
||||
adopted_protocols: adopted.ok ? adopted.value : adopted.error.names,
|
||||
methods: required,
|
||||
optional_methods: optional,
|
||||
location: read.location(address),
|
||||
@@ -195,5 +224,5 @@ export const createObjcProtocolReader = (input: {
|
||||
);
|
||||
}
|
||||
};
|
||||
return { list, record };
|
||||
return { readList, record };
|
||||
};
|
||||
@@ -9,12 +9,12 @@ export const APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE = createAnalysisProfile(
|
||||
{
|
||||
id: NATIVE_MACOS_PROVIDER_IDENTITY.id,
|
||||
name: NATIVE_MACOS_PROVIDER_IDENTITY.name,
|
||||
version: "apple-metadata-reader-2",
|
||||
version: "apple-metadata-reader-3",
|
||||
},
|
||||
{
|
||||
operation: "inspect_native_dispatch_metadata",
|
||||
reader: "apple-dispatch-metadata",
|
||||
revision: 2,
|
||||
revision: 3,
|
||||
},
|
||||
);
|
||||
/** Operations whose target process can show UI, use the network and write files. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
@@ -6,6 +7,70 @@ import { describe, expect } from "vitest";
|
||||
import { machoWithPropertyAttributes } from "../../fixtures/objcPropertyMacho.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { objcAdoptedProtocolCountMacho } from "../../fixtures/objcRecordBudgetMacho.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../../src/domain/native/objcSwiftMetadata.js";
|
||||
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native adopted-protocol list recovery",
|
||||
() => {
|
||||
cliTest.for([1n, 20001n])(
|
||||
"preserves class facts and partial-list semantics over CLI and stdio MCP for count %s",
|
||||
async (count, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-protocol-list-");
|
||||
const path = join(root, "fixture.macho");
|
||||
const bytes = objcAdoptedProtocolCountMacho(count);
|
||||
await writeFile(path, bytes);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
const cliReply = await cli.run({
|
||||
arguments: ["inspect-native-dispatch-metadata", path, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(cliReply.exitCode, cliReply.stderr).toBe(0);
|
||||
const cliResult = nativeDispatchMetadataResultSchema.parse(
|
||||
parseEvidence(cliReply.json).normalized_result,
|
||||
);
|
||||
const call = await cli.sharedMcpCall!();
|
||||
const opened = await call("open_binary", { path });
|
||||
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
||||
try {
|
||||
const reply = await call("inspect_native_dispatch_metadata", {});
|
||||
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
||||
const mcpResult = nativeDispatchMetadataResultSchema.parse(
|
||||
parseEvidence(reply.structuredContent).normalized_result,
|
||||
);
|
||||
for (const result of [cliResult, mcpResult]) {
|
||||
expect(result.target_sha256).toBe(sha256);
|
||||
expect(result.input_admission?.retained_bytes).toBe(bytes.length);
|
||||
expect(result.result.objc_classes).toMatchObject([
|
||||
{
|
||||
name: "FixtureClass",
|
||||
methods: [{ selector: "classMethod" }],
|
||||
protocols: count === 1n ? ["FixtureProtocol"] : [],
|
||||
decode:
|
||||
count === 1n
|
||||
? { status: "decoded", reason: null }
|
||||
: { status: "partial", reason: "protocol_list_unreadable" },
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
result.result.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
)?.status,
|
||||
).toBe(count === 1n ? "complete" : "partial");
|
||||
}
|
||||
expect(mcpResult.result.objc_classes).toEqual(
|
||||
cliResult.result.objc_classes,
|
||||
);
|
||||
} finally {
|
||||
const closed = await call("close_binary", {});
|
||||
expect(closed.isError).not.toBe(true);
|
||||
}
|
||||
expect((await readFile(path)).equals(bytes)).toBe(true);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native dispatch property attributes",
|
||||
|
||||
@@ -1572,7 +1572,7 @@ const expectReaderBindingMismatch = (
|
||||
expect(projectAnalysisError(error).details).toMatchObject({
|
||||
binding_mismatch: {
|
||||
provider: {
|
||||
snapshot: { version: "apple-metadata-reader-older" },
|
||||
snapshot: { version: "apple-metadata-reader-2" },
|
||||
selected: { version: selectedVersion },
|
||||
},
|
||||
profile_digest: { snapshot: snapshotDigest },
|
||||
@@ -1610,12 +1610,13 @@ const expectProfilelessWorkflowSnapshotRejections = async (
|
||||
if (!opened.ok) throw opened.error;
|
||||
const readerIdentity = APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE.provider;
|
||||
const wrongReader = structuredClone(snapshot);
|
||||
// Reader v2 could discard owners when their adopted-protocol list was unreadable.
|
||||
const otherReaderProfile = createAnalysisProfile(
|
||||
{ ...readerIdentity, version: "apple-metadata-reader-older" },
|
||||
{ ...readerIdentity, version: "apple-metadata-reader-2" },
|
||||
{
|
||||
operation: "inspect_native_dispatch_metadata",
|
||||
reader: "apple-dispatch-metadata",
|
||||
revision: 0,
|
||||
revision: 2,
|
||||
},
|
||||
);
|
||||
wrongReader.binding = {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../../../src/domain/native/objcSwiftMetadata.js";
|
||||
import {
|
||||
NativeFixtureRunner,
|
||||
nativeMachoTargetForFile,
|
||||
} from "../../../fixtures/nativeCommands.js";
|
||||
import { objcRecordBudgetMacho } from "../../../fixtures/objcRecordBudgetMacho.js";
|
||||
import {
|
||||
objcAdoptedProtocolCountMacho,
|
||||
objcRecordBudgetMacho,
|
||||
} from "../../../fixtures/objcRecordBudgetMacho.js";
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const sectionCases = [
|
||||
@@ -101,3 +106,39 @@ it("continues decoding the well-formed eight-byte class-list control", async ()
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([1n, 20001n])(
|
||||
"retains class facts and source identity through provider delivery for protocol count %s",
|
||||
async (count) => {
|
||||
const bytes = objcAdoptedProtocolCountMacho(count);
|
||||
const { result } = await inspect(bytes);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
const report = nativeDispatchMetadataResultSchema.parse(
|
||||
result.value.result,
|
||||
);
|
||||
expect(report.target_sha256).toBe(
|
||||
createHash("sha256").update(bytes).digest("hex"),
|
||||
);
|
||||
expect(report.input_admission?.retained_bytes).toBe(bytes.length);
|
||||
expect(report.result.objc_classes).toMatchObject([
|
||||
{
|
||||
name: "FixtureClass",
|
||||
methods: [{ selector: "classMethod", method_type: "instance" }],
|
||||
protocols: count === 1n ? ["FixtureProtocol"] : [],
|
||||
decode:
|
||||
count === 1n
|
||||
? { status: "decoded", reason: null }
|
||||
: { status: "partial", reason: "protocol_list_unreadable" },
|
||||
},
|
||||
]);
|
||||
expect(report.result.objc_dispatch_implementations).toMatchObject([
|
||||
{ class_name: "FixtureClass", selector: "classMethod" },
|
||||
]);
|
||||
expect(
|
||||
report.result.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
)?.status,
|
||||
).toBe(count === 1n ? "complete" : "partial");
|
||||
},
|
||||
);
|
||||
+8
@@ -76,3 +76,11 @@ export const objcRecordBudgetMacho = (
|
||||
bytes.write("protocolMethod\0", 0x9a0);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** A class adopts the fixture protocol through a separately counted list. */
|
||||
export const objcAdoptedProtocolCountMacho = (count: bigint): Buffer => {
|
||||
const bytes = objcRecordBudgetMacho("class");
|
||||
bytes.writeBigUInt64LE(base + 0x6b8n, 0x328);
|
||||
bytes.writeBigUInt64LE(count, 0x6b8);
|
||||
return bytes;
|
||||
};
|
||||
Reference in new issue
Block a user