Files
reverse-engineering-anything/src/domain/symbolAnalysis.objcClasses.test.ts
T
morluto 8defd220a9 refactor(objc): retire obsolete class discovery paths
Remove the test-only selection/projection facade and its unused synthesized binding scan. Keep lexical and identity controls at their active owners, with combined behavior covered through EnhancedTools.
2026-10-24 09:14:21 +08:00

197 lines
5.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
selectObjcClassCandidates,
projectObjcClassCandidates,
} from "./symbolAnalysis.js";
const completeCoverage = { status: "complete" as const, reason: null };
const classImport = {
requested_address: "0x1000",
symbol: "_OBJC_CLASS_$_Document",
binding_source: "bind" as const,
library_ordinal: 1,
library: "/System/Library/Frameworks/AppKit.framework/AppKit",
weak: false,
addend: "0",
};
describe("Objective-C class label discovery", () => {
it("does not reinterpret a provider-specific label without an owner tag", () => {
expect(
selectObjcClassCandidates(
[{ address: "0x1", name: "__objc_class_HuntRoot_class" }],
"Hunt",
),
).toEqual([]);
});
it("uses provider-tagged stripped class labels and filters their exact text", () => {
const labels = [
"__objc_class_HuntRoot_class",
"__objc_class_HuntSub_class",
"__objc_class_HuntWorker_class",
"__objc_class_HuntModel_class",
"__objc_class_HuntLegacy_class",
"__objc_class_Hunt_class_class",
"prefix__objc_class_HuntForeign_class",
"__objc_class__class",
"__objc_class_HuntMissingSuffix",
"__objc_class_HuntExtra_class_suffix",
];
const names = labels.map((name, index) => ({
address: `0x${(index + 1).toString(16)}`,
name,
...(index < 6
? { objc_class_label: "hopper_legacy_class_object" as const }
: {}),
}));
expect(selectObjcClassCandidates(names, "Hunt")).toEqual(names.slice(0, 6));
expect(
selectObjcClassCandidates(names, "__objc_class_HuntRoot_class"),
).toEqual([names[0]]);
});
it("coalesces only selected rows whose own addresses prove the same import", () => {
const alias = { address: "0x1000", name: "OBJC_CLASS_$_Document$GOT" };
const canonical = { address: "0x1000", name: "OBJC_CLASS_$_Document" };
const unrelated = { address: "0x2000", name: "OBJC_CLASS_$_Document" };
const scan = {
bindings: [classImport],
coverage: completeCoverage,
};
expect(
projectObjcClassCandidates([alias, canonical, unrelated], scan),
).toEqual({
count: 2,
classes: [
{
...canonical,
binding_identity: {
symbol: classImport.symbol,
binding_source: classImport.binding_source,
library_ordinal: classImport.library_ordinal,
library: classImport.library,
weak: false,
addend: "0",
slot_addresses: ["0x1000"],
},
},
unrelated,
],
identity_coverage: completeCoverage,
});
});
it("does not join same-name rows without exact slot evidence", () => {
const alias = { address: "0x1000", name: "OBJC_CLASS_$_Document$GOT" };
const canonical = { address: "0x2000", name: "OBJC_CLASS_$_Document" };
expect(
projectObjcClassCandidates([alias, canonical], {
bindings: [classImport],
coverage: completeCoverage,
}),
).toMatchObject({ count: 2, classes: [alias, canonical] });
});
it("keeps rows separate when equal symbols have different bind identities", () => {
const rows = [
{ address: "0x1000", name: "OBJC_CLASS_$_Document$GOT" },
{ address: "0x2000", name: "OBJC_CLASS_$_Document$GOT" },
];
expect(
projectObjcClassCandidates(rows, {
bindings: [
classImport,
{
...classImport,
requested_address: "0x2000",
library: "/other/framework.dylib",
},
],
coverage: completeCoverage,
}),
).toMatchObject({ count: 2, classes: rows });
});
it("deduplicates exact class labels while keeping distinct addresses", () => {
const rows = [
{ address: "0x1000", name: "OBJC_CLASS_$_Document" },
{ address: "0x1000", name: "OBJC_CLASS_$_Document" },
{ address: "0x2000", name: "OBJC_CLASS_$_Document" },
];
expect(selectObjcClassCandidates(rows, "")).toEqual([rows[0], rows[2]]);
});
});
it("keeps equal symbols separate when only their lookup ordinals differ", () => {
const rows = [
{ address: "0x1000", name: "OBJC_CLASS_$_Document$GOT" },
{ address: "0x2000", name: "OBJC_CLASS_$_Document$GOT" },
];
expect(
projectObjcClassCandidates(rows, {
bindings: [
{ ...classImport, library_ordinal: -1, library: null },
{
...classImport,
requested_address: "0x2000",
library_ordinal: -2,
library: null,
},
],
coverage: completeCoverage,
}),
).toMatchObject({
count: 2,
classes: [
{ ...rows[0], binding_identity: { library_ordinal: -1 } },
{ ...rows[1], binding_identity: { library_ordinal: -2 } },
],
});
});
it("keeps regular self-scope and weak rebinding records separate", () => {
const rows = [
{ address: "0x1000", name: "OBJC_CLASS_$_Document$GOT" },
{ address: "0x2000", name: "OBJC_CLASS_$_Document$GOT" },
];
expect(
projectObjcClassCandidates(rows, {
bindings: [
{ ...classImport, library_ordinal: 0, library: null },
{
...classImport,
requested_address: "0x2000",
binding_source: "weak",
library_ordinal: 0,
library: null,
},
],
coverage: completeCoverage,
}),
).toMatchObject({
count: 2,
classes: [
{ ...rows[0], binding_identity: { binding_source: "bind" } },
{ ...rows[1], binding_identity: { binding_source: "weak" } },
],
});
});
it("excludes pointer and string labels that only contain a class marker", () => {
const result = selectObjcClassCandidates(
[
{ address: "0x1", name: "PTR__OBJC_CLASS_$_NSAlert_100028668" },
{ address: "0x2", name: "s__OBJC_CLASS_$_NSAlert_10002ed0d" },
{ address: "0x3", name: "AppKit::_OBJC_CLASS_$_NSAlert" },
],
"",
);
expect(result).toEqual([
{ address: "0x3", name: "AppKit::_OBJC_CLASS_$_NSAlert" },
]);
});