diff --git a/src/ida/IdaMcpConnection.test.ts b/src/ida/IdaMcpConnection.test.ts index 3bd60cf59a..ce1fefba78 100644 --- a/src/ida/IdaMcpConnection.test.ts +++ b/src/ida/IdaMcpConnection.test.ts @@ -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(); diff --git a/src/ida/IdaMcpConnection.ts b/src/ida/IdaMcpConnection.ts index 5c5abbdd57..5f49ac3816 100644 --- a/src/ida/IdaMcpConnection.ts +++ b/src/ida/IdaMcpConnection.ts @@ -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. diff --git a/src/native/AppleDispatchMetadata.test.ts b/src/native/AppleDispatchMetadata.test.ts index aeed9ad65f..81f8559f3d 100644 --- a/src/native/AppleDispatchMetadata.test.ts +++ b/src/native/AppleDispatchMetadata.test.ts @@ -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",C,N,GcustomTitle,V_title', + { firstName: "title" }, + ), + 100, + provenance, + ); + expect(result.objc_classes[0]?.properties).toEqual( + ["title", "second"].map((name) => ({ + name, + type_encoding: '@"NSString"', + atomicity: "nonatomic", + attributes: [ + { name: "T", value: '@"NSString"' }, + { 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 { + 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=},N,V_position"; const result = decodeAppleDispatchMetadata( diff --git a/src/native/AppleMachoFixups.test.ts b/src/native/AppleMachoFixups.test.ts index ce3effe14f..1a9644e2a8 100644 --- a/src/native/AppleMachoFixups.test.ts +++ b/src/native/AppleMachoFixups.test.ts @@ -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",C,N,GcustomTitle,V_title', - ), - ).toEqual({ - name: "title", - type_encoding: '@"NSString"', - atomicity: "nonatomic", - attributes: [ - { name: "T", value: '@"NSString"' }, - { 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=},N,V_position", - ), - ).toMatchObject({ - type_encoding: "{vector=}", - atomicity: "nonatomic", - attributes: [ - { name: "T", value: "{vector=}" }, - { 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 { const parseOrdinal = (ordinal: number, dylibs: readonly string[]) => { const immediate = ordinal < 0 ? ordinal + 16 : ordinal; diff --git a/src/native/AppleObjcProperties.ts b/src/native/AppleObjcProperties.ts index 9c95f6a83c..28db584ada 100644 --- a/src/native/AppleObjcProperties.ts +++ b/src/native/AppleObjcProperties.ts @@ -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