mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(native)!: preserve unknown Objective-C metadata
BREAKING CHANGE: Objective-C attribute tokens now contain only name and value. Properties report nullable atomicity and readonly facts; class flags, ivar counts, and method requirement flags are nullable when unobserved.
This commit is contained in:
1 parent
e10f1f0901
commit
b112d44a39
10 files changed
+225
-45
No files matched your search
@@ -96,8 +96,18 @@ complete observation.
|
||||
|
||||
So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for
|
||||
example `NSObject`). The `pointer_fixups` coverage facet names the mechanism.
|
||||
Class properties, with parsed attributes, and `__objc_catlist` categories are
|
||||
also decoded. Categories record the extended class, local or external, plus
|
||||
Class properties preserve each encoded attribute as a raw `{name, value}`
|
||||
pair, for example `{ name: "N", value: "" }`, and report atomicity on the
|
||||
property: `N` means `nonatomic`; a complete typed attribute string without
|
||||
`N` means the Objective-C default `atomic`;
|
||||
missing or incomplete attribute metadata leaves `atomicity` and any
|
||||
unestablished read-only value null. `is_meta_class`, `is_root_class`, and
|
||||
`ivar_count` are also nullable when the selected provider exposes only symbol
|
||||
names. Class implementation methods have null `is_required` and `is_optional`
|
||||
fields because those facts apply to protocol declarations; decoded protocol
|
||||
method lists retain their observed required/optional values. Class properties
|
||||
and `__objc_catlist` categories are also decoded. Categories record the
|
||||
extended class, local or external, plus
|
||||
their methods, protocols and properties; category methods appear as
|
||||
implementations with a `category`. Swift field-offset globals that are only
|
||||
initialized at runtime stay unresolved. Generic, resilient, async and
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -6,6 +7,12 @@ import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
|
||||
import {
|
||||
artifactCliEvidence,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../../dist/domain/native/objcSwiftMetadata.js";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
@@ -61,6 +68,30 @@ const checkObjc = (metadata, mode) => {
|
||||
const objcCoverage = metadata.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
);
|
||||
assert.equal(fixture?.is_meta_class, false);
|
||||
assert.equal(fixture?.is_root_class, false);
|
||||
assert.equal(
|
||||
fixture?.properties.find(({ name }) => name === "fixtureName")?.atomicity,
|
||||
"nonatomic",
|
||||
);
|
||||
assert.equal(
|
||||
fixture?.properties.find(({ name }) => name === "atomicValue")?.atomicity,
|
||||
"atomic",
|
||||
);
|
||||
for (const method of fixture.methods) {
|
||||
assert.equal(method.is_required, null);
|
||||
assert.equal(method.is_optional, null);
|
||||
}
|
||||
for (const property of fixture.properties)
|
||||
for (const attribute of property.attributes)
|
||||
assert.deepEqual(Object.keys(attribute).sort(), ["name", "value"]);
|
||||
const protocol = metadata.objc_protocols.find(
|
||||
({ name }) => name === "ReaDispatchProtocol",
|
||||
);
|
||||
assert.equal(protocol?.methods[0]?.is_required, true);
|
||||
assert.equal(protocol?.methods[0]?.is_optional, false);
|
||||
assert.equal(protocol?.optional_methods[0]?.is_required, false);
|
||||
assert.equal(protocol?.optional_methods[0]?.is_optional, true);
|
||||
if (
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
@@ -162,6 +193,25 @@ try {
|
||||
await exec("/usr/bin/strip", ["-x", objcPath]);
|
||||
const { target, metadata } = await inspect(objcPath);
|
||||
checkObjc(metadata, mode);
|
||||
if (mode.mode === "chained" && variant === "stripped") {
|
||||
const cli = await artifactCliEvidence(
|
||||
"inspect-native-dispatch-metadata",
|
||||
objcPath,
|
||||
);
|
||||
const cliResult = nativeDispatchMetadataResultSchema.parse(
|
||||
cli.normalized_result,
|
||||
);
|
||||
assert.equal(cliResult.target_sha256, target.sha256);
|
||||
checkObjc(cliResult.result, mode);
|
||||
await withArtifactMcp(objcPath, async (client) => {
|
||||
const mcpResult = nativeDispatchMetadataResultSchema.parse(
|
||||
await artifactMcpResult(client, "inspect_native_dispatch_metadata"),
|
||||
);
|
||||
assert.equal(mcpResult.target_sha256, target.sha256);
|
||||
checkObjc(mcpResult.result, mode);
|
||||
assert.deepEqual(mcpResult.result, cliResult.result);
|
||||
});
|
||||
}
|
||||
results.push({
|
||||
variant: `objc-${mode.mode}-${variant}`,
|
||||
target_sha256: target.sha256,
|
||||
|
||||
@@ -60,6 +60,15 @@ use inclusive ends and can contain gaps. Do not treat the enclosing span as code
|
||||
For packed targets, retain the original and separately derived artifact identities;
|
||||
decompiling an unpacking stub does not recover the unpacked program.
|
||||
|
||||
When reading Objective-C dispatch metadata, distinguish class implementation
|
||||
methods from protocol requirements: class methods report null requirement
|
||||
flags, while decoded protocol declarations retain required and optional facts.
|
||||
Property attributes remain raw name/value tokens; use the property's
|
||||
`atomicity` field for the decoded `N` marker or the default atomic behavior.
|
||||
Null class/meta flags, ivar counts, atomicity, or read-only values mean the
|
||||
selected provider did not establish that fact. Symbol names alone do not prove
|
||||
that a class is a root class or has no ivars.
|
||||
|
||||
## Managed PE/CLI
|
||||
|
||||
Start with `inspect_managed_artifact`. REA's canonical managed inspection is
|
||||
|
||||
@@ -14,6 +14,15 @@ describe("ObjC/Swift metadata", () => {
|
||||
);
|
||||
expect(result.objc_classes).toHaveLength(1);
|
||||
expect(result.objc_dispatch_implementations).toHaveLength(1);
|
||||
expect(result.objc_classes[0]).toMatchObject({
|
||||
is_meta_class: null,
|
||||
is_root_class: null,
|
||||
ivar_count: null,
|
||||
});
|
||||
expect(result.objc_classes[0]?.methods[0]).toMatchObject({
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
});
|
||||
expect(result.swift_symbols).toHaveLength(0);
|
||||
expect(result.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -12,10 +12,6 @@ export const objcMethodTypeSchema = z.enum([
|
||||
export const objcPropertyAttributeSchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
value: z.string(),
|
||||
is_weak: z.boolean().default(false),
|
||||
is_atomic: z.boolean().default(false),
|
||||
is_copy: z.boolean().default(false),
|
||||
is_strong: z.boolean().default(false),
|
||||
});
|
||||
|
||||
/** Objective-C method metadata. */
|
||||
@@ -23,8 +19,8 @@ export const objcMethodSchema = z.strictObject({
|
||||
selector: z.string().min(1),
|
||||
method_type: objcMethodTypeSchema,
|
||||
address: z.number().int().nullable(),
|
||||
is_required: z.boolean().default(false),
|
||||
is_optional: z.boolean().default(false),
|
||||
is_required: z.boolean().nullable(),
|
||||
is_optional: z.boolean().nullable(),
|
||||
});
|
||||
type ObjcMethod = z.infer<typeof objcMethodSchema>;
|
||||
|
||||
@@ -32,8 +28,9 @@ type ObjcMethod = z.infer<typeof objcMethodSchema>;
|
||||
export const objcPropertySchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
type_encoding: z.string().nullable(),
|
||||
attributes: z.array(objcPropertyAttributeSchema).default([]),
|
||||
is_readonly: z.boolean().default(false),
|
||||
attributes: z.array(objcPropertyAttributeSchema),
|
||||
atomicity: z.enum(["atomic", "nonatomic"]).nullable(),
|
||||
is_readonly: z.boolean().nullable(),
|
||||
getter: z.string().nullable(),
|
||||
setter: z.string().nullable(),
|
||||
});
|
||||
@@ -50,12 +47,12 @@ export const objcProtocolSchema = z.strictObject({
|
||||
export const objcClassSchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
super_class: z.string().nullable(),
|
||||
is_meta_class: z.boolean().default(false),
|
||||
is_root_class: z.boolean().default(false),
|
||||
is_meta_class: z.boolean().nullable(),
|
||||
is_root_class: z.boolean().nullable(),
|
||||
methods: z.array(objcMethodSchema).default([]),
|
||||
properties: z.array(objcPropertySchema).default([]),
|
||||
protocols: z.array(z.string()).default([]),
|
||||
ivar_count: z.number().int().nonnegative().default(0),
|
||||
ivar_count: z.number().int().nonnegative().nullable(),
|
||||
instance_size: z.number().int().nonnegative().nullable(),
|
||||
location: z
|
||||
.object({
|
||||
@@ -380,20 +377,20 @@ export const inspectNativeDispatchMetadata = (
|
||||
selector: implementation.selector,
|
||||
method_type: implementation.method_type,
|
||||
address: parseMetadataAddress(implementation.implementation_address),
|
||||
is_required: false,
|
||||
is_optional: false,
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
});
|
||||
methodsByClass.set(implementation.class_name, methods);
|
||||
}
|
||||
const classes: ObjcClass[] = [...classNames].sort().map((name) => ({
|
||||
name,
|
||||
super_class: null,
|
||||
is_meta_class: false,
|
||||
is_root_class: false,
|
||||
is_meta_class: null,
|
||||
is_root_class: null,
|
||||
methods: methodsByClass.get(name) ?? [],
|
||||
properties: [],
|
||||
protocols: [],
|
||||
ivar_count: 0,
|
||||
ivar_count: null,
|
||||
instance_size: null,
|
||||
}));
|
||||
const decodedImplementations = [...implementations.values()].sort(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { decodeAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
|
||||
|
||||
const base = 0x100000000n;
|
||||
const fixture = (relative = false) => {
|
||||
const bytes = Buffer.alloc(2048);
|
||||
const bytes = Buffer.alloc(4096);
|
||||
const u32 = (offset: number, value: number) =>
|
||||
bytes.writeUInt32LE(value, offset);
|
||||
const ptr = (offset: number, target: number) =>
|
||||
@@ -16,8 +16,8 @@ const fixture = (relative = false) => {
|
||||
u32(36, 152);
|
||||
bytes.write("__DATA", 40);
|
||||
ptr(56, 0);
|
||||
bytes.writeBigUInt64LE(2048n, 64);
|
||||
bytes.writeBigUInt64LE(2048n, 80);
|
||||
bytes.writeBigUInt64LE(4096n, 64);
|
||||
bytes.writeBigUInt64LE(4096n, 80);
|
||||
u32(92, 5);
|
||||
u32(96, 1);
|
||||
bytes.write("__objc_classlist", 104);
|
||||
@@ -33,6 +33,8 @@ const fixture = (relative = false) => {
|
||||
ptr(0x318, 0x450);
|
||||
ptr(0x320, 0x480);
|
||||
ptr(0x330, 0x500);
|
||||
ptr(0x328, 0x700);
|
||||
ptr(0x340, 0x600);
|
||||
u32(0x380, 1);
|
||||
ptr(0x398, 0x450);
|
||||
bytes.write("Fixture\0", 0x450);
|
||||
@@ -56,6 +58,34 @@ const fixture = (relative = false) => {
|
||||
u32(0x580, 8);
|
||||
bytes.write("state\0", 0x5a0);
|
||||
bytes.write("i\0", 0x5c0);
|
||||
u32(0x600, 16);
|
||||
u32(0x604, 3);
|
||||
ptr(0x608, 0x640);
|
||||
ptr(0x610, 0x650);
|
||||
ptr(0x618, 0x660);
|
||||
ptr(0x620, 0x670);
|
||||
ptr(0x628, 0x680);
|
||||
ptr(0x630, 0x690);
|
||||
bytes.write("atomicValue\0", 0x640);
|
||||
bytes.write("Tq\0", 0x650);
|
||||
bytes.write("nonatomicValue\0", 0x660);
|
||||
bytes.write("Tq,N\0", 0x670);
|
||||
bytes.write("unknownValue\0", 0x680);
|
||||
bytes.writeBigUInt64LE(1n, 0x700);
|
||||
ptr(0x708, 0x740);
|
||||
ptr(0x748, 0x900);
|
||||
ptr(0x758, 0x800);
|
||||
ptr(0x768, 0x840);
|
||||
u32(0x780, 72);
|
||||
u32(0x800, 24);
|
||||
u32(0x804, 1);
|
||||
ptr(0x808, 0x920);
|
||||
u32(0x840, 24);
|
||||
u32(0x844, 1);
|
||||
ptr(0x848, 0x940);
|
||||
bytes.write("FixtureProtocol\0", 0x900);
|
||||
bytes.write("requiredAction:\0", 0x920);
|
||||
bytes.write("optionalAction:\0", 0x940);
|
||||
return bytes;
|
||||
};
|
||||
const provenance = { path: "/fixture/app", sha256: "a".repeat(64) };
|
||||
@@ -92,6 +122,69 @@ describe("Apple dispatch binary metadata", () => {
|
||||
location: { file_offset: 0x508 },
|
||||
},
|
||||
]);
|
||||
expect(result.objc_classes[0]?.properties).toEqual([
|
||||
{
|
||||
name: "atomicValue",
|
||||
type_encoding: "q",
|
||||
attributes: [{ name: "T", value: "q" }],
|
||||
atomicity: "atomic",
|
||||
is_readonly: false,
|
||||
getter: null,
|
||||
setter: null,
|
||||
},
|
||||
{
|
||||
name: "nonatomicValue",
|
||||
type_encoding: "q",
|
||||
attributes: [
|
||||
{ name: "T", value: "q" },
|
||||
{ name: "N", value: "" },
|
||||
],
|
||||
atomicity: "nonatomic",
|
||||
is_readonly: false,
|
||||
getter: null,
|
||||
setter: null,
|
||||
},
|
||||
{
|
||||
name: "unknownValue",
|
||||
type_encoding: null,
|
||||
attributes: [],
|
||||
atomicity: null,
|
||||
is_readonly: null,
|
||||
getter: null,
|
||||
setter: null,
|
||||
},
|
||||
]);
|
||||
expect(result.objc_classes[0]).toMatchObject({
|
||||
is_meta_class: false,
|
||||
is_root_class: false,
|
||||
ivar_count: 1,
|
||||
});
|
||||
expect(result.objc_classes[1]).toMatchObject({
|
||||
is_meta_class: true,
|
||||
is_root_class: false,
|
||||
ivar_count: null,
|
||||
});
|
||||
expect(result.objc_classes[0]?.methods[0]).toMatchObject({
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
});
|
||||
expect(result.objc_protocols[0]).toMatchObject({
|
||||
name: "FixtureProtocol",
|
||||
methods: [
|
||||
{
|
||||
selector: "requiredAction:",
|
||||
is_required: true,
|
||||
is_optional: false,
|
||||
},
|
||||
],
|
||||
optional_methods: [
|
||||
{
|
||||
selector: "optionalAction:",
|
||||
is_required: false,
|
||||
is_optional: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
it("keeps unsupported pointers and record truncation explicit", () => {
|
||||
|
||||
@@ -275,28 +275,27 @@ describe("Objective-C property attributes", () => {
|
||||
).toEqual({
|
||||
name: "title",
|
||||
type_encoding: '@"NSString<NSCopying, NSSecureCoding>"',
|
||||
atomicity: "nonatomic",
|
||||
attributes: [
|
||||
expect.objectContaining({
|
||||
name: "T",
|
||||
value: '@"NSString<NSCopying, NSSecureCoding>"',
|
||||
}),
|
||||
expect.objectContaining({ name: "C", is_copy: true }),
|
||||
expect.objectContaining({ name: "N" }),
|
||||
expect.objectContaining({ name: "G", value: "customTitle" }),
|
||||
expect.objectContaining({ name: "V", value: "_title" }),
|
||||
{ name: "T", value: '@"NSString<NSCopying, NSSecureCoding>"' },
|
||||
{ name: "C", value: "" },
|
||||
{ name: "N", value: "" },
|
||||
{ name: "G", value: "customTitle" },
|
||||
{ name: "V", value: "_title" },
|
||||
],
|
||||
is_readonly: false,
|
||||
getter: "customTitle",
|
||||
setter: null,
|
||||
});
|
||||
expect(parsePropertyAttributes("count", "Tq,R,N")).toMatchObject({
|
||||
expect(parsePropertyAttributes("count", "Tq,R")).toMatchObject({
|
||||
type_encoding: "q",
|
||||
is_readonly: true,
|
||||
atomicity: "atomic",
|
||||
});
|
||||
expect(
|
||||
parsePropertyAttributes("delegate", "T@,W,N,V_delegate").attributes[1],
|
||||
).toMatchObject({
|
||||
is_weak: true,
|
||||
});
|
||||
expect(parsePropertyAttributes("count", "Tq,R,N").atomicity).toBe(
|
||||
"nonatomic",
|
||||
);
|
||||
expect(parsePropertyAttributes("count", "").atomicity).toBeNull();
|
||||
expect(parsePropertyAttributes("count", "R").atomicity).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -216,7 +216,7 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
`Superclass of ${name} requires external binding resolution`,
|
||||
);
|
||||
const ivarList = read.pointer(ro + 48n);
|
||||
let ivarCount = 0;
|
||||
let ivarCount: number | null = meta ? null : 0;
|
||||
if (ivarList !== 0n && !meta) {
|
||||
const stride = read.u32(ivarList),
|
||||
count = read.u32(ivarList + 4n);
|
||||
@@ -276,8 +276,8 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
BigInt(Number.MAX_SAFE_INTEGER)
|
||||
? Number(BigInt(item.implementation_address))
|
||||
: null,
|
||||
is_required: false,
|
||||
is_optional: false,
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
})),
|
||||
properties: readObjcPropertiesOf(read, read.pointer(ro + 64n), {
|
||||
owner: name,
|
||||
|
||||
@@ -21,20 +21,32 @@ export const parsePropertyAttributes = (
|
||||
attributes: string,
|
||||
): ObjcProperty => {
|
||||
const tokens = attributes.length === 0 ? [] : splitAttributes(attributes);
|
||||
const typeAttributes = tokens.filter((token) => token[0] === "T");
|
||||
const typeAttribute =
|
||||
typeAttributes.length === 1 ? typeAttributes[0] : undefined;
|
||||
const typeEncoding =
|
||||
typeAttribute !== undefined && typeAttribute.length > 1
|
||||
? typeAttribute.slice(1)
|
||||
: null;
|
||||
const value = (code: string): string | null =>
|
||||
tokens.find((token) => token.startsWith(code))?.slice(1) ?? null;
|
||||
tokens.find((token) => token[0] === code)?.slice(1) ?? null;
|
||||
return {
|
||||
name,
|
||||
type_encoding: value("T"),
|
||||
type_encoding: typeEncoding,
|
||||
attributes: tokens.map((token) => ({
|
||||
name: token.slice(0, 1),
|
||||
value: token.slice(1),
|
||||
is_weak: token === "W",
|
||||
is_atomic: false,
|
||||
is_copy: token === "C",
|
||||
is_strong: token === "&",
|
||||
})),
|
||||
is_readonly: tokens.includes("R"),
|
||||
atomicity: tokens.includes("N")
|
||||
? "nonatomic"
|
||||
: typeEncoding === null
|
||||
? null
|
||||
: "atomic",
|
||||
is_readonly: tokens.includes("R")
|
||||
? true
|
||||
: typeEncoding === null
|
||||
? null
|
||||
: false,
|
||||
getter: value("G"),
|
||||
setter: value("S"),
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
int state;
|
||||
}
|
||||
@property(nonatomic, copy) NSString *fixtureName;
|
||||
@property NSInteger atomicValue;
|
||||
- (void)performAction:(id)sender;
|
||||
+ (int)fixtureVersion;
|
||||
@end
|
||||
|
||||
Reference in new issue
Block a user