refactor(providers): retire obsolete decoding paths

Remove the test-only Objective-C property facade that discarded completeness. Verify its remaining semantics through portable Mach-O metadata decoding, including retained partial properties and located issues.

Decode the single IDA text result directly while preserving native JSON and structured-content precedence. Update the stale SDK deadline assertion to check the busy tool through its diagnostic owner.
This commit is contained in:
morluto committed 2026-10-24 08:27:26 +08:00
1 parent e8b2273ab6
commit 1c07bee5b6
5 files changed
+127 -96

No files matched your search

+1
View File
@@ -335,6 +335,7 @@ it("bounds a stalled health poll by the remaining readiness deadline", async ()
});
if (!(failure instanceof AnalysisTimeoutError))
throw new Error("Expected the readiness deadline to expire");
expect(failure.userMessage).toContain("config_json_get");
expect(failure.capturedOutput).toBeDefined();
} finally {
await connection.close();
+9 -11
View File
@@ -126,12 +126,13 @@ export const decodeIdaToolResult = (
);
// Native MCP retains the complete JSON text while structuredContent may
// contain a shortened preview. Parse that producer-owned representation once.
const text = blocks.length === 1 ? blocks[0] : undefined;
if (protocol === "native") {
if (blocks.length !== 1)
if (text === undefined)
throw new AnalysisProtocolError(
"Native IDA MCP omitted its complete JSON text result.",
);
return parseIdaJsonText(blocks[0] ?? "");
return parseIdaJsonText(text);
}
if (result.structuredContent !== undefined) {
const value = result.structuredContent;
@@ -141,18 +142,15 @@ export const decodeIdaToolResult = (
: value,
);
}
if (blocks.length !== 1)
if (text === undefined)
throw new AnalysisProtocolError(
"IDA MCP omitted a single structured or text result; unsupported content cannot establish an empty observation.",
);
const values = blocks.map((text): JsonValue => {
try {
return jsonValueSchema.parse(JSON.parse(text));
} catch {
return text;
}
});
return values[0] ?? null;
try {
return jsonValueSchema.parse(JSON.parse(text));
} catch {
return text;
}
};
// Retain only producer-sanitized JSON diagnostics, never a native parser cause.
+117
View File
@@ -120,6 +120,123 @@ const fixture = (
};
const provenance = { path: "/fixture/app", sha256: "a".repeat(64) };
describe("decoded Objective-C property attributes", () => {
it("retains quoted class names, ownership and accessors", () => {
const result = decodeAppleDispatchMetadata(
machoWithPropertyAttributes(
'T@"NSString<NSCopying, NSSecureCoding>",C,N,GcustomTitle,V_title',
{ firstName: "title" },
),
100,
provenance,
);
expect(result.objc_classes[0]?.properties).toEqual(
["title", "second"].map((name) => ({
name,
type_encoding: '@"NSString<NSCopying, NSSecureCoding>"',
atomicity: "nonatomic",
attributes: [
{ 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(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({ status: "complete", reason: null });
});
it.each([
["Tq,R", "q", "atomic"],
["Tq,R,N", "q", "nonatomic"],
["R", null, null],
] as const)(
"preserves readonly flags and established atomicity for %s",
(attributes, typeEncoding, atomicity) => {
const result = decodeAppleDispatchMetadata(
machoWithPropertyAttributes(attributes),
100,
provenance,
);
expect(result.objc_classes[0]?.properties).toMatchObject(
["first", "second"].map((name) => ({
name,
type_encoding: typeEncoding,
atomicity,
is_readonly: true,
})),
);
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({ status: "complete", reason: null });
},
);
it.each([
[
"an unfinished quoted class name",
'T@"NSString,N,R',
'@"NSString,N,R',
null,
null,
],
[
"flags before an unfinished quote",
'N,R,T@"NSString',
'@"NSString',
"nonatomic",
true,
],
[
"an unfinished template argument",
"T{vector<float, float,N,R",
"{vector<float, float,N,R",
null,
null,
],
] as const)(
"retains established flags and reports both properties with %s",
(_, attributes, typeEncoding, atomicity, readonly) => {
const result = decodeAppleDispatchMetadata(
machoWithPropertyAttributes(attributes),
100,
provenance,
);
expect(result.objc_classes[0]?.properties).toMatchObject(
["first", "second"].map((name) => ({
name,
type_encoding: typeEncoding,
atomicity,
is_readonly: readonly,
})),
);
const coverage = result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
);
expect(coverage).toMatchObject({ status: "partial" });
expect(coverage?.reason).toContain(
"property_attributes_incomplete at 0x100000508: Fixture: property first",
);
expect(coverage?.reason).toContain(
"property_attributes_incomplete at 0x100000518: Fixture: property second",
);
expect(
coverage?.reason?.match(/property_attributes_incomplete at /gu),
).toHaveLength(2);
},
);
});
it("retains Objective-C++ template commas in decoded property facts", () => {
const attributes = "T{vector<float, float, float>=},N,V_position";
const result = decodeAppleDispatchMetadata(
-76
View File
@@ -4,7 +4,6 @@ import {
decodeChainedPointer,
parsePointerFixups,
} from "./AppleMachoFixups.js";
import { parsePropertyAttributes } from "./AppleObjcProperties.js";
import type { Segment } from "./AppleMachoSelection.js";
const BASE = 0x100000000n;
@@ -1685,81 +1684,6 @@ describe("legacy bind operand decoding", () => {
});
});
describe("Objective-C property attributes", () => {
it("parses type, ownership, accessors and quoted class names", () => {
expect(
parsePropertyAttributes(
"title",
'T@"NSString<NSCopying, NSSecureCoding>",C,N,GcustomTitle,V_title',
),
).toEqual({
name: "title",
type_encoding: '@"NSString<NSCopying, NSSecureCoding>"',
atomicity: "nonatomic",
attributes: [
{ 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")).toMatchObject({
type_encoding: "q",
is_readonly: true,
atomicity: "atomic",
});
expect(parsePropertyAttributes("count", "Tq,R,N").atomicity).toBe(
"nonatomic",
);
expect(parsePropertyAttributes("count", "").atomicity).toBeNull();
expect(parsePropertyAttributes("count", "R").atomicity).toBeNull();
});
it("keeps template argument commas inside an Objective-C++ type", () => {
expect(
parsePropertyAttributes(
"position",
"T{vector<float, float, float>=},N,V_position",
),
).toMatchObject({
type_encoding: "{vector<float, float, float>=}",
atomicity: "nonatomic",
attributes: [
{ name: "T", value: "{vector<float, float, float>=}" },
{ name: "N", value: "" },
{ name: "V", value: "_position" },
],
});
});
it("establishes no defaults from an unfinished quoted class name", () => {
// `,N,R` here lie inside the unterminated quote, so they are not flags.
expect(parsePropertyAttributes("title", 'T@"NSString,N,R')).toMatchObject({
type_encoding: '@"NSString,N,R',
atomicity: null,
is_readonly: null,
});
expect(parsePropertyAttributes("title", 'N,R,T@"NSString')).toMatchObject({
atomicity: "nonatomic",
is_readonly: true,
});
});
it("keeps defaults unknown when a template argument is unfinished", () => {
expect(
parsePropertyAttributes("position", "T{vector<float, float,N,R"),
).toMatchObject({
type_encoding: "{vector<float, float,N,R",
atomicity: null,
is_readonly: null,
});
});
});
it("preserves classic special ordinals and returns unsupported at invalid bind locations", () => {
const parseOrdinal = (ordinal: number, dylibs: readonly string[]) => {
const immediate = ordinal < 0 ? ordinal + 16 : ordinal;
-9
View File
@@ -12,15 +12,6 @@ export interface ObjcPropertyReaders {
string(address: bigint): string;
}
/**
* Parse an `@property` attribute string such as `T@"NSString",C,N,V_name`.
* Each comma-separated token is one attribute; `T` is the type encoding.
*/
export const parsePropertyAttributes = (
name: string,
attributes: string,
): ObjcProperty => decodePropertyAttributes(name, attributes).property;
/**
* Decode one attribute string. An unfinished quoted or templated type leaves
* later attributes ambiguous, so a missing `N` or `R` does not establish the