mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
- compose.cpp: show static fields for root structs even when collapsed - test_compose: set collapsed=false on nodes needing expanded rendering - test_disasm: set collapsed=false on vtable pointer nodes - test_static_fields: rewrite collapsed test to use non-root child struct
538 lines
18 KiB
C++
538 lines
18 KiB
C++
#include <QtTest/QTest>
|
|
#include "core.h"
|
|
#include "addressparser.h"
|
|
|
|
using namespace rcx;
|
|
|
|
class TestStaticFields : public QObject {
|
|
Q_OBJECT
|
|
|
|
// Convenience: build a struct with N regular fields + static fields
|
|
struct TestTree {
|
|
NodeTree tree;
|
|
uint64_t rootId = 0;
|
|
|
|
TestTree() {
|
|
Node root;
|
|
root.kind = NodeKind::Struct;
|
|
root.name = "TestStruct";
|
|
root.structTypeName = "TestStruct";
|
|
root.parentId = 0;
|
|
int ri = tree.addNode(root);
|
|
rootId = tree.nodes[ri].id;
|
|
}
|
|
|
|
int addField(const QString& name, NodeKind kind, int offset) {
|
|
Node f;
|
|
f.kind = kind;
|
|
f.name = name;
|
|
f.parentId = rootId;
|
|
f.offset = offset;
|
|
return tree.addNode(f);
|
|
}
|
|
|
|
int addStaticField(const QString& name, const QString& expr,
|
|
NodeKind kind = NodeKind::Hex64) {
|
|
Node h;
|
|
h.kind = kind;
|
|
h.name = name;
|
|
h.parentId = rootId;
|
|
h.offset = 0;
|
|
h.isStatic = true;
|
|
h.offsetExpr = expr;
|
|
return tree.addNode(h);
|
|
}
|
|
};
|
|
|
|
private slots:
|
|
|
|
// ── Basic properties ──
|
|
|
|
void testStaticFieldFlag() {
|
|
TestTree t;
|
|
int hi = t.addStaticField("h", "base");
|
|
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
|
QCOMPARE(t.tree.nodes[hi].offsetExpr, QStringLiteral("base"));
|
|
}
|
|
|
|
void testRegularFieldNotStatic() {
|
|
TestTree t;
|
|
int fi = t.addField("x", NodeKind::UInt32, 0);
|
|
QCOMPARE(t.tree.nodes[fi].isStatic, false);
|
|
QCOMPARE(t.tree.nodes[fi].offsetExpr, QString());
|
|
}
|
|
|
|
void testStaticFieldIsChild() {
|
|
TestTree t;
|
|
int hi = t.addStaticField("h", "base");
|
|
QCOMPARE(t.tree.nodes[hi].parentId, t.rootId);
|
|
auto children = t.tree.childrenOf(t.rootId);
|
|
QVERIFY(children.contains(hi));
|
|
}
|
|
|
|
// ── JSON serialization ──
|
|
|
|
void testStaticFieldJsonRoundTrip() {
|
|
TestTree t;
|
|
t.addField("e_lfanew", NodeKind::UInt32, 0x3C);
|
|
t.addStaticField("nt_hdr", "base + e_lfanew", NodeKind::Struct);
|
|
|
|
QJsonObject json = t.tree.toJson();
|
|
NodeTree t2 = NodeTree::fromJson(json);
|
|
|
|
QCOMPARE(t2.nodes.size(), 3);
|
|
// root
|
|
QCOMPARE(t2.nodes[0].isStatic, false);
|
|
// field
|
|
QCOMPARE(t2.nodes[1].isStatic, false);
|
|
QCOMPARE(t2.nodes[1].name, QStringLiteral("e_lfanew"));
|
|
// static field
|
|
QCOMPARE(t2.nodes[2].isStatic, true);
|
|
QCOMPARE(t2.nodes[2].offsetExpr, QStringLiteral("base + e_lfanew"));
|
|
QCOMPARE(t2.nodes[2].name, QStringLiteral("nt_hdr"));
|
|
QCOMPARE(t2.nodes[2].kind, NodeKind::Struct);
|
|
}
|
|
|
|
void testStaticFieldJsonBackwardCompat() {
|
|
// Old JSON without isStatic should default to false
|
|
NodeTree tree;
|
|
Node root;
|
|
root.kind = NodeKind::Struct;
|
|
root.name = "Old";
|
|
root.parentId = 0;
|
|
tree.addNode(root);
|
|
|
|
QJsonObject json = tree.toJson();
|
|
NodeTree t2 = NodeTree::fromJson(json);
|
|
QCOMPARE(t2.nodes[0].isStatic, false);
|
|
QCOMPARE(t2.nodes[0].offsetExpr, QString());
|
|
}
|
|
|
|
void testMultipleStaticFieldsRoundTrip() {
|
|
TestTree t;
|
|
t.addField("ptr", NodeKind::Pointer64, 0);
|
|
t.addStaticField("h1", "base");
|
|
t.addStaticField("h2", "base + ptr");
|
|
t.addStaticField("h3", "base + 0x100");
|
|
|
|
QJsonObject json = t.tree.toJson();
|
|
NodeTree t2 = NodeTree::fromJson(json);
|
|
|
|
int staticFieldCount = 0;
|
|
for (const auto& n : t2.nodes)
|
|
if (n.isStatic) staticFieldCount++;
|
|
QCOMPARE(staticFieldCount, 3);
|
|
}
|
|
|
|
// ── Struct span exclusion ──
|
|
|
|
void testStructSpanExcludesStaticFields() {
|
|
TestTree t;
|
|
t.addField("a", NodeKind::UInt32, 0); // 0+4 = 4
|
|
t.addField("b", NodeKind::UInt64, 4); // 4+8 = 12
|
|
t.addStaticField("h", "base"); // should NOT affect span
|
|
|
|
QCOMPARE(t.tree.structSpan(t.rootId), 12);
|
|
}
|
|
|
|
void testStructSpanWithOnlyStaticFields() {
|
|
TestTree t;
|
|
t.addStaticField("h1", "base");
|
|
t.addStaticField("h2", "base + 0x100");
|
|
|
|
// No regular fields -> span = 0
|
|
QCOMPARE(t.tree.structSpan(t.rootId), 0);
|
|
}
|
|
|
|
void testStructSpanMixedOrder() {
|
|
// Static fields interleaved with regular fields
|
|
TestTree t;
|
|
t.addField("x", NodeKind::Float, 0); // 0+4 = 4
|
|
t.addStaticField("h1", "base");
|
|
t.addField("y", NodeKind::Float, 4); // 4+4 = 8
|
|
t.addStaticField("h2", "base + x");
|
|
t.addField("z", NodeKind::Float, 8); // 8+4 = 12
|
|
|
|
QCOMPARE(t.tree.structSpan(t.rootId), 12);
|
|
}
|
|
|
|
// ── Address expression evaluation ──
|
|
|
|
void testExprBase() {
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0x1000; }
|
|
*ok = false; return 0;
|
|
};
|
|
auto r = AddressParser::evaluate("base", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x1000);
|
|
}
|
|
|
|
void testExprBaseAddHex() {
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0x1000; }
|
|
*ok = false; return 0;
|
|
};
|
|
auto r = AddressParser::evaluate("base + 0x3C", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x103C);
|
|
}
|
|
|
|
void testExprBaseAddField() {
|
|
// Simulate: base=0x1000, e_lfanew value=0xE8
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0x1000; }
|
|
if (name == "e_lfanew") { *ok = true; return 0xE8; }
|
|
*ok = false; return 0;
|
|
};
|
|
auto r = AddressParser::evaluate("base + e_lfanew", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x10E8);
|
|
}
|
|
|
|
void testExprSubtraction() {
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0x2000; }
|
|
*ok = false; return 0;
|
|
};
|
|
auto r = AddressParser::evaluate("base - 0x10", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x1FF0);
|
|
}
|
|
|
|
void testExprMultiplication() {
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0x100; }
|
|
if (name == "index") { *ok = true; return 3; }
|
|
*ok = false; return 0;
|
|
};
|
|
auto r = AddressParser::evaluate("base + index * 8", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x118); // 0x100 + 3*8
|
|
}
|
|
|
|
void testExprUnresolvedIdentifier() {
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0x1000; }
|
|
*ok = false; return 0;
|
|
};
|
|
auto r = AddressParser::evaluate("base + unknown_field", 8, &cbs);
|
|
QVERIFY(!r.ok);
|
|
}
|
|
|
|
void testExprPureHex() {
|
|
auto r = AddressParser::evaluate("0x7FF600000000", 8, nullptr);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x7FF600000000ULL);
|
|
}
|
|
|
|
void testExprParentheses() {
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0x1000; }
|
|
if (name == "offset") { *ok = true; return 0x10; }
|
|
*ok = false; return 0;
|
|
};
|
|
auto r = AddressParser::evaluate("(base + offset) * 2", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x2020); // (0x1000 + 0x10) * 2
|
|
}
|
|
|
|
void testExprEmptyString() {
|
|
auto r = AddressParser::evaluate("", 8, nullptr);
|
|
QVERIFY(!r.ok);
|
|
}
|
|
|
|
// ── Static field with BufferProvider (simulates live resolution) ──
|
|
|
|
void testStaticFieldResolveFromBuffer() {
|
|
// Build tree: struct with UInt32 "offset_field" at +0x10
|
|
// Static field expression: "base + offset_field"
|
|
// Buffer has 0x000000E8 at address 0x10
|
|
QByteArray data(64, '\0');
|
|
// Write 0xE8 at offset 0x10 (little-endian uint32)
|
|
data[0x10] = (char)0xE8;
|
|
data[0x11] = 0;
|
|
data[0x12] = 0;
|
|
data[0x13] = 0;
|
|
|
|
BufferProvider prov(data);
|
|
|
|
// Build resolver mimicking compose.cpp's makeResolver
|
|
uint64_t baseAddr = 0; // buffer starts at 0
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [&prov, baseAddr](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return baseAddr; }
|
|
if (name == "offset_field") {
|
|
uint64_t addr = baseAddr + 0x10;
|
|
if (prov.isReadable(addr, 4)) {
|
|
*ok = true;
|
|
return (uint64_t)prov.readU32(addr);
|
|
}
|
|
}
|
|
*ok = false; return 0;
|
|
};
|
|
|
|
auto r = AddressParser::evaluate("base + offset_field", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0xE8); // 0 + 0xE8
|
|
}
|
|
|
|
void testStaticFieldResolvePointerChain() {
|
|
// Buffer: addr 0x00 has pointer to 0x20, addr 0x20 has pointer to 0x40
|
|
QByteArray data(64, '\0');
|
|
// Write pointer at 0x00 -> 0x20 (little-endian uint64)
|
|
data[0x00] = 0x20;
|
|
// Write pointer at 0x20 -> 0x40
|
|
data[0x20] = 0x40;
|
|
|
|
BufferProvider prov(data);
|
|
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
|
if (name == "base") { *ok = true; return 0; }
|
|
*ok = false; return 0;
|
|
};
|
|
cbs.readPointer = [&prov](uint64_t addr, bool* ok) -> uint64_t {
|
|
if (prov.isReadable(addr, 8)) {
|
|
*ok = true;
|
|
return prov.readU64(addr);
|
|
}
|
|
*ok = false; return 0;
|
|
};
|
|
|
|
// [base] = deref pointer at base (0x00) -> 0x20
|
|
auto r = AddressParser::evaluate("[base]", 8, &cbs);
|
|
QVERIFY(r.ok);
|
|
QCOMPARE(r.value, (uint64_t)0x20);
|
|
|
|
// [[base]] = double deref: [0x00]->0x20, [0x20]->0x40
|
|
auto r2 = AddressParser::evaluate("[[base]]", 8, &cbs);
|
|
QVERIFY(r2.ok);
|
|
QCOMPARE(r2.value, (uint64_t)0x40);
|
|
}
|
|
|
|
// ── Compose output ──
|
|
|
|
void testComposeStaticFieldHeader() {
|
|
TestTree t;
|
|
t.addField("x", NodeKind::Float, 0);
|
|
t.addStaticField("h", "base");
|
|
|
|
NullProvider prov;
|
|
ComposeResult result = compose(t.tree, prov);
|
|
|
|
// Static field header should contain "static" keyword
|
|
bool foundStatic = false;
|
|
QStringList lines = result.text.split('\n');
|
|
for (const auto& line : lines) {
|
|
if (line.contains(QStringLiteral("static ")) && line.contains(QStringLiteral("{"))) {
|
|
foundStatic = true;
|
|
break;
|
|
}
|
|
}
|
|
QVERIFY2(foundStatic, "Static field header line not found in compose output");
|
|
}
|
|
|
|
void testComposeStaticFieldLine() {
|
|
TestTree t;
|
|
t.addField("x", NodeKind::Float, 0);
|
|
t.addStaticField("h", "base");
|
|
|
|
NullProvider prov;
|
|
ComposeResult result = compose(t.tree, prov);
|
|
|
|
// Find the static field line and check its meta
|
|
bool foundStaticField = false;
|
|
for (const auto& lm : result.meta) {
|
|
if (lm.isStaticLine) {
|
|
foundStaticField = true;
|
|
break;
|
|
}
|
|
}
|
|
QVERIFY2(foundStaticField, "Static field line metadata not found in compose output");
|
|
}
|
|
|
|
void testComposeNoStaticFieldsWhenCollapsed() {
|
|
// Use a non-root struct to test collapsed behavior
|
|
// (root structs are always expanded via isRootHeader)
|
|
NodeTree tree;
|
|
Node root;
|
|
root.kind = NodeKind::Struct;
|
|
root.name = "Root";
|
|
root.parentId = 0;
|
|
int ri = tree.addNode(root);
|
|
uint64_t rootId = tree.nodes[ri].id;
|
|
|
|
Node child;
|
|
child.kind = NodeKind::Struct;
|
|
child.name = "Child";
|
|
child.parentId = rootId;
|
|
child.offset = 0;
|
|
child.collapsed = true; // collapsed child struct
|
|
int ci = tree.addNode(child);
|
|
uint64_t childId = tree.nodes[ci].id;
|
|
|
|
Node f;
|
|
f.kind = NodeKind::Float;
|
|
f.name = "x";
|
|
f.parentId = childId;
|
|
f.offset = 0;
|
|
tree.addNode(f);
|
|
|
|
Node sf;
|
|
sf.kind = NodeKind::Hex64;
|
|
sf.name = "h";
|
|
sf.parentId = childId;
|
|
sf.offset = 0;
|
|
sf.isStatic = true;
|
|
sf.offsetExpr = QStringLiteral("base");
|
|
tree.addNode(sf);
|
|
|
|
NullProvider prov;
|
|
ComposeResult result = compose(tree, prov);
|
|
|
|
// When collapsed, no static field lines should appear
|
|
for (const auto& lm : result.meta)
|
|
QVERIFY2(!lm.isStaticLine,
|
|
"Static field line should not appear when struct is collapsed");
|
|
}
|
|
|
|
void testComposeStaticFieldExprDisplay() {
|
|
TestTree t;
|
|
t.addField("offset", NodeKind::UInt32, 0);
|
|
t.addStaticField("target", "base + offset");
|
|
|
|
NullProvider prov;
|
|
ComposeResult result = compose(t.tree, prov);
|
|
|
|
// Static field line should contain the expression text
|
|
bool foundExpr = false;
|
|
QStringList lines = result.text.split('\n');
|
|
for (const auto& line : lines) {
|
|
if (line.contains(QStringLiteral("base + offset"))) {
|
|
foundExpr = true;
|
|
break;
|
|
}
|
|
}
|
|
QVERIFY2(foundExpr, "Static field expression not found in compose output");
|
|
}
|
|
|
|
void testComposeStaticFieldsAfterRegularFields() {
|
|
TestTree t;
|
|
t.addField("a", NodeKind::UInt32, 0);
|
|
t.addField("b", NodeKind::UInt64, 4);
|
|
t.addStaticField("h", "base");
|
|
|
|
NullProvider prov;
|
|
ComposeResult result = compose(t.tree, prov);
|
|
|
|
// Find meta indices: last regular field vs first static field line
|
|
int lastFieldMeta = -1;
|
|
int firstStaticFieldMeta = -1;
|
|
for (int i = 0; i < result.meta.size(); i++) {
|
|
if (result.meta[i].lineKind == LineKind::Field
|
|
&& !result.meta[i].isStaticLine)
|
|
lastFieldMeta = i;
|
|
if (result.meta[i].isStaticLine && firstStaticFieldMeta < 0)
|
|
firstStaticFieldMeta = i;
|
|
}
|
|
|
|
QVERIFY(lastFieldMeta >= 0);
|
|
QVERIFY(firstStaticFieldMeta >= 0);
|
|
QVERIFY2(firstStaticFieldMeta > lastFieldMeta,
|
|
"Static field lines must come after all regular fields");
|
|
}
|
|
|
|
// ── Node byteSize for static fields ──
|
|
|
|
void testStaticFieldByteSize() {
|
|
// Static field nodes should still report their kind's byte size
|
|
Node h;
|
|
h.kind = NodeKind::Hex64;
|
|
h.isStatic = true;
|
|
h.offsetExpr = "base";
|
|
QCOMPARE(h.byteSize(), 8);
|
|
|
|
h.kind = NodeKind::Struct;
|
|
QCOMPARE(h.byteSize(), 0); // struct static fields have 0 size (children determine)
|
|
}
|
|
|
|
// ── Children ordering ──
|
|
|
|
void testChildrenOfIncludesStaticFields() {
|
|
TestTree t;
|
|
t.addField("a", NodeKind::UInt32, 0);
|
|
int hi = t.addStaticField("h", "base");
|
|
|
|
auto children = t.tree.childrenOf(t.rootId);
|
|
QCOMPARE(children.size(), 2);
|
|
QVERIFY(children.contains(hi));
|
|
}
|
|
|
|
// ── Edge cases ──
|
|
|
|
void testStaticFieldWithEmptyExpr() {
|
|
TestTree t;
|
|
Node h;
|
|
h.kind = NodeKind::Hex64;
|
|
h.name = "h";
|
|
h.parentId = t.rootId;
|
|
h.isStatic = true;
|
|
h.offsetExpr = QString(); // empty expression
|
|
int hi = t.tree.addNode(h);
|
|
|
|
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
|
QCOMPARE(t.tree.nodes[hi].offsetExpr, QString());
|
|
|
|
// JSON round-trip should preserve empty expr
|
|
QJsonObject json = t.tree.toJson();
|
|
NodeTree t2 = NodeTree::fromJson(json);
|
|
QCOMPARE(t2.nodes[hi].isStatic, true);
|
|
}
|
|
|
|
void testStaticFieldStructType() {
|
|
// Static field can be a struct (pointing to a different address)
|
|
TestTree t;
|
|
int hi = t.addStaticField("nt_headers", "base + 0xE8", NodeKind::Struct);
|
|
t.tree.nodes[hi].structTypeName = "IMAGE_NT_HEADERS";
|
|
|
|
QCOMPARE(t.tree.nodes[hi].kind, NodeKind::Struct);
|
|
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
|
QCOMPARE(t.tree.nodes[hi].structTypeName, QStringLiteral("IMAGE_NT_HEADERS"));
|
|
}
|
|
|
|
void testStaticFieldPointerType() {
|
|
// Static field can be a pointer type
|
|
TestTree t;
|
|
int hi = t.addStaticField("indirect", "base + 0x20", NodeKind::Pointer64);
|
|
|
|
QCOMPARE(t.tree.nodes[hi].kind, NodeKind::Pointer64);
|
|
QCOMPARE(t.tree.nodes[hi].isStatic, true);
|
|
}
|
|
|
|
// ── Validate expression syntax ──
|
|
|
|
void testExprValidate() {
|
|
// Valid expressions
|
|
QCOMPARE(AddressParser::validate("base"), QString());
|
|
QCOMPARE(AddressParser::validate("base + 0x10"), QString());
|
|
QCOMPARE(AddressParser::validate("0x1000"), QString());
|
|
QCOMPARE(AddressParser::validate("(base + offset) * 2"), QString());
|
|
|
|
// Invalid expressions
|
|
QVERIFY(!AddressParser::validate("").isEmpty());
|
|
QVERIFY(!AddressParser::validate("+ +").isEmpty());
|
|
QVERIFY(!AddressParser::validate("(base").isEmpty());
|
|
}
|
|
};
|
|
|
|
QTEST_MAIN(TestStaticFields)
|
|
#include "test_static_fields.moc"
|