Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c723269cb | ||
|
|
5412c7361b |
No files matched your search
@@ -183,7 +183,10 @@ export const analyzeInterfaceBuilderBundle = async (input: {
|
||||
...(numberLiterals === undefined ? {} : { numberLiterals }),
|
||||
...(nib === null || nib.omitted === 0
|
||||
? {}
|
||||
: { omittedSerializedHierarchyReferences: nib.omitted }),
|
||||
: {
|
||||
omittedSerializedHierarchyReferences: nib.omitted,
|
||||
serializedHierarchyLimitReached: nib.limitReached,
|
||||
}),
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
if (input.signal?.aborted === true) throw cause;
|
||||
@@ -607,6 +610,7 @@ const projectNibArchive = (
|
||||
});
|
||||
}
|
||||
let hierarchyOmitted = 0;
|
||||
let hierarchyLimitReached = false;
|
||||
let hierarchyNodeCount = 0;
|
||||
const hierarchyFor = (
|
||||
objectId: number,
|
||||
@@ -619,6 +623,7 @@ const projectNibArchive = (
|
||||
}
|
||||
if (hierarchyNodeCount >= maxHierarchyNodes) {
|
||||
hierarchyOmitted += 1;
|
||||
hierarchyLimitReached = true;
|
||||
return null;
|
||||
}
|
||||
if (!budget.tryReserve(128)) {
|
||||
@@ -680,6 +685,10 @@ const projectNibArchive = (
|
||||
);
|
||||
return {
|
||||
omitted: viewHierarchy.omitted + hierarchyOmitted + merged.omitted,
|
||||
limitReached:
|
||||
hierarchyLimitReached ||
|
||||
viewHierarchy.limitReached ||
|
||||
merged.limitReached,
|
||||
unresolvedConnections,
|
||||
raw: jsonValueSchema.parse({
|
||||
"com.apple.ibtool.document.objects": objects,
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
expect(projectNibViewHierarchy([object(0)], new Set([0]))).toEqual({
|
||||
hierarchy: null,
|
||||
omitted: 0,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +38,7 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
},
|
||||
],
|
||||
omitted: 0,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,6 +46,7 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
expect(projectNibViewHierarchy([object(0, 99)], new Set([0]))).toEqual({
|
||||
hierarchy: [],
|
||||
omitted: 1,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +62,11 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
],
|
||||
new Set([0]),
|
||||
),
|
||||
).toEqual({ hierarchy: [{ objectID: "0", children: [] }], omitted: 1 });
|
||||
).toEqual({
|
||||
hierarchy: [{ objectID: "0", children: [] }],
|
||||
omitted: 1,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("marks unsupported serialized parent values as incomplete", () => {
|
||||
@@ -68,7 +75,7 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
[{ id: 0, class_name: "NSView", values: { NSSuperview: "unknown" } }],
|
||||
new Set([0]),
|
||||
),
|
||||
).toEqual({ hierarchy: null, omitted: 1 });
|
||||
).toEqual({ hierarchy: null, omitted: 1, limitReached: false });
|
||||
});
|
||||
|
||||
it("reports cyclic and self-referential parentage as incomplete", () => {
|
||||
@@ -80,6 +87,7 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
).toEqual({
|
||||
hierarchy: [],
|
||||
omitted: 3,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,6 +97,7 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
).toEqual({
|
||||
hierarchy: [],
|
||||
omitted: 1,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,10 +115,10 @@ describe("serialized NIB view hierarchy bounds", () => {
|
||||
const objects = Array.from({ length: 20_002 }, (_, id) =>
|
||||
object(id, id === 0 ? undefined : 0),
|
||||
);
|
||||
// The record bound is the caller's object budget, so the omission names it.
|
||||
expect(
|
||||
projectNibViewHierarchy(objects, new Set(objects.map(({ id }) => id)))
|
||||
.omitted,
|
||||
).toBe(2);
|
||||
projectNibViewHierarchy(objects, new Set(objects.map(({ id }) => id))),
|
||||
).toMatchObject({ omitted: 2, limitReached: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,6 +172,7 @@ describe("legacy NIB hierarchy preservation", () => {
|
||||
{ objectID: "parent", children: [{ objectID: "child", children: [] }] },
|
||||
],
|
||||
omitted: 0,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
it("reports conflicting encoded parents rather than dropping a link silently", () => {
|
||||
@@ -184,6 +194,7 @@ describe("legacy NIB hierarchy preservation", () => {
|
||||
expect(mergeNibHierarchies(legacy, recovered)).toEqual({
|
||||
hierarchy: legacy,
|
||||
omitted: 1,
|
||||
limitReached: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,8 @@ export const projectNibViewHierarchy = (
|
||||
objects,
|
||||
retainedIds,
|
||||
);
|
||||
if (parents.size === 0) return { hierarchy: null, omitted: invalid };
|
||||
if (parents.size === 0)
|
||||
return { hierarchy: null, omitted: invalid, limitReached: false };
|
||||
const children = new Map<number, number[]>();
|
||||
for (const [child, parent] of parents) {
|
||||
if (!retainedIds.has(parent) || !retainedIds.has(child)) continue;
|
||||
@@ -31,9 +32,11 @@ export const projectNibViewHierarchy = (
|
||||
for (const id of involved)
|
||||
if (!parents.has(id)) pending.push({ id, depth: 0, output: roots });
|
||||
const visited = new Set<number>();
|
||||
let limitReached = false;
|
||||
while (pending.length > 0) {
|
||||
const item = pending.pop();
|
||||
if (item === undefined) break;
|
||||
if (!visited.has(item.id) && visited.size >= maxNodes) limitReached = true;
|
||||
if (
|
||||
visited.has(item.id) ||
|
||||
item.depth > 128 ||
|
||||
@@ -50,6 +53,7 @@ export const projectNibViewHierarchy = (
|
||||
return {
|
||||
hierarchy: roots,
|
||||
omitted: involved.size - visited.size + invalid,
|
||||
limitReached,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,6 +87,7 @@ export const mergeNibHierarchies = (
|
||||
);
|
||||
}
|
||||
let omitted = 0;
|
||||
let limitReached = false;
|
||||
const additions = recovered.map((node) => ({
|
||||
node,
|
||||
output: legacy,
|
||||
@@ -94,6 +99,7 @@ export const mergeNibHierarchies = (
|
||||
let target = existing.get(entry.node.objectID);
|
||||
if (target === undefined) {
|
||||
if (existing.size >= maxNodes || !reserveNode()) {
|
||||
if (existing.size >= maxNodes) limitReached = true;
|
||||
omitted += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -126,7 +132,7 @@ export const mergeNibHierarchies = (
|
||||
parent: target.objectID,
|
||||
});
|
||||
}
|
||||
return { hierarchy: legacy, omitted };
|
||||
return { hierarchy: legacy, omitted, limitReached };
|
||||
};
|
||||
|
||||
const wouldCycle = (
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface InterfaceBuilderDocumentInput {
|
||||
readonly numberLiterals?: PlistNumberLiterals;
|
||||
/** Serialized hierarchy links omitted before normalized graph projection. */
|
||||
readonly omittedSerializedHierarchyReferences?: number;
|
||||
/** Whether the object budget, rather than the archive, caused that omission. */
|
||||
readonly serializedHierarchyLimitReached?: boolean;
|
||||
}
|
||||
|
||||
type InvestigationNode = z.infer<typeof nativeInvestigationNodeSchema>;
|
||||
@@ -155,6 +157,9 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
let connectionEdgeCount = 0;
|
||||
let hierarchyEdgeCount = 0;
|
||||
let documentOmittedConnections = 0;
|
||||
// Edges dropped by the connection edge budget, as opposed to connection
|
||||
// edges whose source node did not fit the graph object budget.
|
||||
let documentConnectionBudgetOmissions = 0;
|
||||
let documentOmittedHierarchyEdges = 0;
|
||||
let hierarchyTruncated = false;
|
||||
const objectNodeId = (objectId: string): string =>
|
||||
@@ -175,6 +180,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
} else {
|
||||
omittedConnections += 1;
|
||||
documentOmittedConnections += 1;
|
||||
documentConnectionBudgetOmissions += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -508,13 +514,15 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
? "partial"
|
||||
: "complete",
|
||||
reason:
|
||||
connectionsTruncated || documentOmittedConnections > 0
|
||||
connectionsTruncated || documentConnectionBudgetOmissions > 0
|
||||
? "connection_limit_reached"
|
||||
: connectionsUnresolved
|
||||
? "connection_unresolved"
|
||||
: unsupportedConnections
|
||||
? "unsupported_connection_type"
|
||||
: null,
|
||||
: documentOmittedConnections > 0
|
||||
? "graph_limit_reached"
|
||||
: connectionsUnresolved
|
||||
? "connection_unresolved"
|
||||
: unsupportedConnections
|
||||
? "unsupported_connection_type"
|
||||
: null,
|
||||
examined: Math.min(
|
||||
parsed.connections.length,
|
||||
input.limits.max_connections,
|
||||
@@ -535,7 +543,9 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
parserOmittedHierarchyReferences > 0
|
||||
? "archive_hierarchy_references_missing"
|
||||
: omittedSerializedHierarchyReferences > 0
|
||||
? "serialized_view_hierarchy_incomplete"
|
||||
? document.serializedHierarchyLimitReached === true
|
||||
? "graph_limit_reached"
|
||||
: "serialized_view_hierarchy_incomplete"
|
||||
: parsed.hierarchy.length === 0
|
||||
? "archive_hierarchy_not_decoded"
|
||||
: hierarchyTruncated
|
||||
|
||||
@@ -547,7 +547,7 @@ describe("bounded Interface Builder archive decoding", () => {
|
||||
expect.objectContaining({
|
||||
facet: "hierarchy:RepeatedHierarchy.nib",
|
||||
status: "partial",
|
||||
reason: "serialized_view_hierarchy_incomplete",
|
||||
reason: "graph_limit_reached",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1404,6 +1404,57 @@ describe("NIBArchive UIKit event connections", () => {
|
||||
});
|
||||
|
||||
describe("NIBArchive ownerless connector accounting", () => {
|
||||
it("attributes connections without retained endpoints to the object budget", async () => {
|
||||
const root = await createTestTempDirectory("rea-ib-object-limit-");
|
||||
const bundle = join(root, "Example.app");
|
||||
await mkdir(bundle, { recursive: true });
|
||||
await writeFile(
|
||||
join(bundle, "Cell.nib"),
|
||||
encodeNibArchiveFixture({
|
||||
classes: [
|
||||
"UIProxyObject",
|
||||
"UIClassSwapper",
|
||||
"UIButton",
|
||||
"UIRuntimeEventConnection",
|
||||
"NSString",
|
||||
],
|
||||
objects: [
|
||||
{ classIndex: 0, values: { UIProxiedObjectIdentifier: { ref: 4 } } },
|
||||
{ classIndex: 1, values: { UIClassName: { ref: 5 } } },
|
||||
{ classIndex: 2, values: {} },
|
||||
{
|
||||
classIndex: 3,
|
||||
values: {
|
||||
UISource: { ref: 2 },
|
||||
UIDestination: { ref: 1 },
|
||||
UILabel: { ref: 6 },
|
||||
UIEventMask: 64,
|
||||
},
|
||||
},
|
||||
{ classIndex: 4, values: { "NS.bytes": "IBFilesOwner" } },
|
||||
{ classIndex: 4, values: { "NS.bytes": "BuildCell" } },
|
||||
{ classIndex: 4, values: { "NS.bytes": "buildTapped:" } },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const analysis = await analyzeInterfaceBuilderBundle({
|
||||
bundlePath: bundle,
|
||||
targetSha256: "e".repeat(64),
|
||||
limits: { max_objects: 1 },
|
||||
});
|
||||
|
||||
// The connection fits its own budget; only the object budget dropped its
|
||||
// source, so raising that budget is what recovers it.
|
||||
expect(analysis.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "connections:Cell.nib",
|
||||
status: "partial",
|
||||
reason: "graph_limit_reached",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a connection dropped by the limit reported as a limit", async () => {
|
||||
const root = await createTestTempDirectory("rea-ib-ownerless-limit-");
|
||||
const bundle = join(root, "Example.app");
|
||||
|
||||
@@ -178,10 +178,13 @@ describe.skipIf(!xcodeIbtoolAvailable)(
|
||||
});
|
||||
expect(result.documents[0]?.hierarchy_complete).toBe(false);
|
||||
expect(result.graph.truncated).toBe(true);
|
||||
// Raising the object budget recovers these links, so the reason must
|
||||
// name that budget rather than an incomplete serialized hierarchy.
|
||||
expect(result.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "hierarchy:Contents/Resources/Main.nib",
|
||||
status: "partial",
|
||||
reason: "graph_limit_reached",
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user