feat: switch provider addressing from RVA to absolute, add pointer expansion tests

This commit is contained in:
IChooseYou
2026-02-18 13:07:48 -07:00
parent fa0d9a377b
commit 26217f5de8
20 changed files with 813 additions and 173 deletions

View File

@@ -1017,7 +1017,7 @@ private slots:
void testPrimitiveArrayElements() {
// Expanded primitive array should synthesize element lines dynamically
NodeTree tree;
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -1934,7 +1934,7 @@ private slots:
void testTextIsNonEmpty() {
// Verify composed text is actually generated (not empty)
NodeTree tree;
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;

View File

@@ -8,7 +8,7 @@
using namespace rcx;
static void buildTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;

View File

@@ -22,7 +22,6 @@ public:
}
int size() const override { return m_data.size(); }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
bool isLive() const override { return true; }
QString name() const override { return QStringLiteral("test"); }
QString kind() const override { return QStringLiteral("Process"); }
@@ -31,7 +30,7 @@ public:
// Small tree: one root struct with a few typed fields at known offsets.
// Keeps tests fast and deterministic (no giant PEB tree).
static void buildSmallTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -405,7 +404,8 @@ private slots:
// ── Test: source switch preserves existing base address ──
void testSourceSwitchPreservesBase() {
// Document already has baseAddress = 0x1000 from buildSmallTree()
// Set a non-zero baseAddress to simulate a loaded .rcx file
m_doc->tree.baseAddress = 0x1000;
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
@@ -414,16 +414,14 @@ private slots:
QCOMPARE(newBase, (uint64_t)0x400000);
m_doc->provider = prov;
// This is the controller logic under test:
// Controller logic: keep existing baseAddress when non-zero
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// baseAddress must stay at the original value
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// provider base must be synced to match
QCOMPARE(m_doc->provider->base(), (uint64_t)0x1000);
// provider base is unchanged (no setBase sync) — provider reports its own initial base
QCOMPARE(m_doc->provider->base(), (uint64_t)0x400000);
}
// ── Test: source switch on fresh doc uses provider default ──
@@ -437,12 +435,9 @@ private slots:
m_doc->provider = prov;
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// Fresh doc should adopt the provider's default base
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
QCOMPARE(m_doc->provider->base(), (uint64_t)0x7FFE0000);
}
// ── Test: toggleCollapse + undo ──

View File

@@ -133,19 +133,18 @@ private slots:
// ──────────────────────────────────────────────────
void testVTableDisasm_composedAddress() {
// Memory layout (provider-relative, i.e. offset from baseAddress):
// Memory layout (absolute addresses, baseAddress = 0):
//
// [0x0000] Root "Obj" struct
// +0x00: Pointer64 __vptr => points to 0xBASE+0x100 (vtable)
// +0x00: Pointer64 __vptr => points to 0x100 (vtable)
//
// [0x0100] VTable (expanded via pointer deref)
// +0x00: func ptr 0 => value 0xBASE+0x200 (func0 code)
// +0x08: func ptr 1 => value 0xBASE+0x300 (func1 code)
// +0x00: func ptr 0 => value 0x200 (func0 code)
// +0x08: func ptr 1 => value 0x300 (func1 code)
//
// [0x0200] func0 code: push rbp; ret
// [0x0300] func1 code: xor eax, eax; ret
//
const uint64_t kBase = 0x7FF600000000ULL;
// Build a 4KB buffer
QByteArray mem(4096, '\0');
@@ -153,12 +152,12 @@ private slots:
memcpy(mem.data() + off, &val, 8);
};
// Root object at offset 0: __vptr points to vtable at kBase + 0x100
w64(0x00, kBase + 0x100);
// Root object at offset 0: __vptr points to vtable at 0x100
w64(0x00, 0x100);
// VTable at offset 0x100: two function pointers
w64(0x100, kBase + 0x200); // slot 0 -> func0
w64(0x108, kBase + 0x300); // slot 1 -> func1
w64(0x100, 0x200); // slot 0 -> func0
w64(0x108, 0x300); // slot 1 -> func1
// func0 at offset 0x200: push rbp; ret
mem[0x200] = '\x55';
@@ -173,7 +172,7 @@ private slots:
// Build node tree
NodeTree tree;
tree.baseAddress = kBase;
tree.baseAddress = 0;
// Root struct "Obj"
Node root;
@@ -227,8 +226,8 @@ private slots:
for (int i = 0; i < result.meta.size(); i++) {
const LineMeta& lm = result.meta[i];
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
// Only include the pointer-expanded ones (near vtable at kBase+0x100)
if (lm.offsetAddr >= kBase + 0x100 && lm.offsetAddr < kBase + 0x200) {
// Only include the pointer-expanded ones (near vtable at 0x100)
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
int nodeIdx = lm.nodeIdx;
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
@@ -239,29 +238,29 @@ private slots:
QCOMPARE(funcPtrs.size(), 2);
// Verify composed addresses point to the vtable, NOT to the root struct
// func0 should be at kBase + 0x100 (vtable + 0)
QCOMPARE(funcPtrs[0].offsetAddr, kBase + 0x100);
// func1 should be at kBase + 0x108 (vtable + 8)
QCOMPARE(funcPtrs[1].offsetAddr, kBase + 0x108);
// func0 should be at 0x100 (vtable + 0)
QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
// func1 should be at 0x108 (vtable + 8)
QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
// Now simulate what the hover code should do:
// Read the function pointer VALUE from the correct provider address
for (const auto& fp : funcPtrs) {
// Provider-relative address = offsetAddr - baseAddress
uint64_t provAddr = fp.offsetAddr - kBase;
// Provider reads at absolute address directly
uint64_t provAddr = fp.offsetAddr;
// Read the pointer value (the function address)
uint64_t ptrVal = prov.readU64(provAddr);
// Verify we got the right pointer values
if (fp.name == "func0") {
QCOMPARE(ptrVal, kBase + 0x200);
QCOMPARE(ptrVal, (uint64_t)0x200);
} else {
QCOMPARE(ptrVal, kBase + 0x300);
QCOMPARE(ptrVal, (uint64_t)0x300);
}
// Convert pointer value to provider-relative for reading code bytes
uint64_t codeProvAddr = ptrVal - kBase;
// Read code bytes at the pointer target (absolute address)
uint64_t codeProvAddr = ptrVal;
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
// Disassemble and verify
@@ -275,14 +274,14 @@ private slots:
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
// Verify address in output matches the real function address
QVERIFY2(lines[0].startsWith("00007ff600000200"),
QVERIFY2(lines[0].contains("200"),
qPrintable("func0 addr wrong: " + lines[0]));
} else {
// Should decode: xor eax, eax; ret
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_)));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
QVERIFY2(lines[0].startsWith("00007ff600000300"),
QVERIFY2(lines[0].contains("300"),
qPrintable("func1 addr wrong: " + lines[0]));
}
}
@@ -292,26 +291,25 @@ private slots:
// inside the ROOT struct, not the vtable.
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
// wrongVal0 = kBase + 0x100 (the vptr itself, NOT a function address)
QCOMPARE(wrongVal0, kBase + 0x100);
// wrongVal0 = 0x100 (the vptr itself, NOT a function address)
QCOMPARE(wrongVal0, (uint64_t)0x100);
// This is the vtable address, not a function — disassembling it would be wrong
QVERIFY2(wrongVal0 != kBase + 0x200,
QVERIFY2(wrongVal0 != (uint64_t)0x200,
"node.offset reads the vptr, not the function pointer");
QVERIFY2(wrongVal1 != kBase + 0x300,
QVERIFY2(wrongVal1 != (uint64_t)0x300,
"node.offset=8 reads past vptr, not the second function pointer");
}
void testVTableDisasm_wrongAddressGivesWrongCode() {
// Demonstrate that using node.offset instead of composed address
// gives completely wrong disassembly results
const uint64_t kBase = 0x10000;
QByteArray mem(1024, '\0');
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
// Root at 0: vptr -> 0x80
w64(0x00, kBase + 0x80);
w64(0x00, (uint64_t)0x80);
// VTable at 0x80: one func ptr -> 0x100
w64(0x80, kBase + 0x100);
w64(0x80, (uint64_t)0x100);
// Code at 0x100: sub rsp, 0x28; nop; ret
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
@@ -320,15 +318,15 @@ private slots:
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
uint64_t wrongPtrVal = prov.readU64(0);
QCOMPARE(wrongPtrVal, kBase + 0x80); // This is the vtable addr, not a function!
QCOMPARE(wrongPtrVal, (uint64_t)0x80); // This is the vtable addr, not a function!
// RIGHT: read from composed address (vtable + 0)
uint64_t rightPtrVal = prov.readU64(0x80);
QCOMPARE(rightPtrVal, kBase + 0x100); // This IS the function address
QCOMPARE(rightPtrVal, (uint64_t)0x100); // This IS the function address
// Disassemble the RIGHT target
QByteArray rightCode = prov.readBytes(0x100, 128);
QString rightAsm = disassemble(rightCode, kBase + 0x100, 64, 128);
QString rightAsm = disassemble(rightCode, 0x100, 64, 128);
QStringList rightLines = rightAsm.split('\n');
QVERIFY(rightLines.size() >= 3);
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
@@ -337,7 +335,7 @@ private slots:
// Disassemble the WRONG target (vtable data, not code!)
QByteArray wrongCode = prov.readBytes(0x80, 128);
QString wrongAsm = disassemble(wrongCode, kBase + 0x80, 64, 128);
QString wrongAsm = disassemble(wrongCode, 0x80, 64, 128);
// The wrong bytes are the vtable entries (pointer values),
// which decode as garbage instructions, not sub/nop/ret
QVERIFY2(!wrongAsm.contains("sub rsp"),
@@ -348,9 +346,9 @@ private slots:
// Full simulation of the hover flow as implemented in editor.cpp:
//
// 1. Compose the tree to get LineMeta with correct offsetAddr
// 2. For each FuncPtr64 line, read pointer value from snapshot/provider
// using lm.offsetAddr - baseAddress (composed address)
// 3. Read code bytes from the REAL provider using ptrVal - baseAddress
// 2. For each FuncPtr64 line, read pointer value from provider
// using lm.offsetAddr (absolute address)
// 3. Read code bytes from the REAL provider using ptrVal directly
// (the real provider can read any process address; snapshot cannot)
// 4. Disassemble the code bytes
//
@@ -358,28 +356,25 @@ private slots:
// the snapshot), step 3 reads from arbitrary code addresses (needs
// the real provider, not snapshot).
const uint64_t kBase = 0x7FF600000000ULL;
QByteArray mem(8192, '\0');
auto w64 = [&](int off, uint64_t val) {
memcpy(mem.data() + off, &val, 8);
};
// Layout:
// [0x000] Root struct: __vptr -> vtable at kBase + 0x100
// [0x100] VTable: func0 -> kBase + 0x1000, func1 -> kBase + 0x1800
// [0x000] Root struct: __vptr -> vtable at 0x100
// [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
// [0x1800] func1 code: xor eax, eax; ret
w64(0x000, kBase + 0x100); // __vptr
w64(0x100, kBase + 0x1000); // vtable[0]
w64(0x108, kBase + 0x1800); // vtable[1]
w64(0x000, (uint64_t)0x100); // __vptr
w64(0x100, (uint64_t)0x1000); // vtable[0]
w64(0x108, (uint64_t)0x1800); // vtable[1]
// func0 code
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
// func1 code
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
// This provider represents the real process memory.
// In production, this is the ProcessMemoryProvider that reads via
// ReadProcessMemory at m_base + addr.
BufferProvider realProv(mem);
// Build a snapshot that only contains tree-data pages (like the
@@ -392,7 +387,7 @@ private slots:
// Build node tree
NodeTree tree;
tree.baseAddress = kBase;
tree.baseAddress = 0;
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
root.parentId = 0; root.offset = 0;
@@ -423,11 +418,11 @@ private slots:
const LineMeta& lm = result.meta[i];
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
continue;
if (lm.offsetAddr < kBase + 0x100 || lm.offsetAddr >= kBase + 0x200)
if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
continue; // skip standalone VTable definition entries
// --- Hover step 1: read pointer value from snapshot ---
uint64_t provAddr = lm.offsetAddr - tree.baseAddress;
uint64_t provAddr = lm.offsetAddr;
// The snapshot has this data (vtable pages are in it)
QVERIFY2(snapProv.isReadable(provAddr, 8),
qPrintable(QString("Snapshot should have vtable page at %1")
@@ -437,7 +432,7 @@ private slots:
// --- Hover step 2: read code from REAL provider ---
// The snapshot does NOT have the code pages:
uint64_t codeAddr = ptrVal - tree.baseAddress;
uint64_t codeAddr = ptrVal;
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
"Snapshot should NOT have function code pages");
// But the real provider does:

View File

@@ -152,7 +152,7 @@ static BufferProvider makeTestProvider() {
// Build the full _PEB64 tree (0x7D0 bytes), unions mapped to first member
static NodeTree makeTestTree() {
NodeTree tree;
tree.baseAddress = 0x000000D87B5E5000ULL;
tree.baseAddress = 0;
// Root struct
Node root;
@@ -342,6 +342,95 @@ static NodeTree makeTestTree() {
return tree;
}
// ── Pointer expansion demo data ──
// Small tree with a working pointer that points within the buffer.
// Root struct "Demo" has a UInt32 "id" and Pointer64 "pChild" → ChildData.
// ChildData has UInt32 "x", UInt32 "y", Float "z".
struct PtrDemo {
NodeTree tree;
BufferProvider prov{QByteArray()};
uint64_t rootId = 0;
uint64_t childStructId = 0;
};
static PtrDemo makePtrDemo(bool collapsed = false, bool nullPtr = false) {
PtrDemo d;
d.tree.baseAddress = 0;
// Root struct
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "Demo";
root.name = "demo";
root.parentId = 0;
root.offset = 0;
int ri = d.tree.addNode(root);
d.rootId = d.tree.nodes[ri].id;
// id field at offset 0
{
Node n;
n.kind = NodeKind::UInt32;
n.name = "id";
n.parentId = d.rootId;
n.offset = 0;
d.tree.addNode(n);
}
// ChildData struct definition (separate root)
Node child;
child.kind = NodeKind::Struct;
child.structTypeName = "ChildData";
child.name = "ChildData";
child.parentId = 0;
child.offset = 200; // standalone rendering offset
int ci = d.tree.addNode(child);
d.childStructId = d.tree.nodes[ci].id;
{
Node n;
n.kind = NodeKind::UInt32; n.name = "x";
n.parentId = d.childStructId; n.offset = 0;
d.tree.addNode(n);
n.kind = NodeKind::UInt32; n.name = "y";
n.offset = 4;
d.tree.addNode(n);
n.kind = NodeKind::Float; n.name = "z";
n.offset = 8;
d.tree.addNode(n);
}
// Pointer at offset 8 → ChildData
{
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "pChild";
ptr.parentId = d.rootId;
ptr.offset = 8;
ptr.refId = d.childStructId;
ptr.collapsed = collapsed;
d.tree.addNode(ptr);
}
// Buffer: 128 bytes
QByteArray data(128, '\0');
uint32_t idVal = 42;
memcpy(data.data() + 0, &idVal, 4);
if (!nullPtr) {
uint64_t ptrVal = 64; // points to offset 64 in buffer
memcpy(data.data() + 8, &ptrVal, 8);
}
// Data at the pointer target (offset 64)
uint32_t xVal = 100; memcpy(data.data() + 64, &xVal, 4);
uint32_t yVal = 200; memcpy(data.data() + 68, &yVal, 4);
float zVal = 3.14f; memcpy(data.data() + 72, &zVal, 4);
d.prov = BufferProvider(data, "ptr_demo");
return d;
}
class TestEditor : public QObject {
Q_OBJECT
private:
@@ -1258,7 +1347,7 @@ private slots:
// Build a small tree: root struct with mixed regular (non-hex) + hex fields
NodeTree tree;
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -1522,6 +1611,440 @@ private slots:
"found %1 / %2 total (see menu_hover_full.png, menu_hover_item.png)")
.arg(amberPixels).arg(totalPixels)));
}
void testStructPreviewPopupOnCollapsedTypedPointer() {
// Build a small tree: root struct with a typed Pointer64 → target struct
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "TestRoot";
root.name = "Root";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Target struct with some fields
Node target;
target.kind = NodeKind::Struct;
target.structTypeName = "TargetStruct";
target.name = "TargetStruct";
target.parentId = 0;
target.offset = 0;
int ti = tree.addNode(target);
uint64_t targetId = tree.nodes[ti].id;
// Add fields to the target struct
{
Node f; f.parentId = targetId;
f.kind = NodeKind::UInt64; f.name = "FieldA"; f.offset = 0;
tree.addNode(f);
f.kind = NodeKind::UInt64; f.name = "FieldB"; f.offset = 8;
tree.addNode(f);
f.kind = NodeKind::UInt32; f.name = "FieldC"; f.offset = 16;
tree.addNode(f);
}
// Add a Pointer64 node that references the target struct, collapsed
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "pTarget";
ptr.parentId = rootId;
ptr.offset = 0;
ptr.refId = targetId;
ptr.collapsed = true;
tree.addNode(ptr);
// Provider: 8 bytes at offset 0 holding a pointer value
QByteArray data(64, '\0');
uint64_t ptrVal = 0x00007FFE12340000ULL;
memcpy(data.data(), &ptrVal, 8);
BufferProvider prov(data, "test_struct_preview");
ComposeResult cr = compose(tree, prov);
m_editor->applyDocument(cr);
m_editor->setProviderRef(&prov, nullptr, &tree);
QApplication::processEvents();
// Find the pointer line (should be a Pointer64 with foldCollapsed=true)
int ptrLine = -1;
for (int i = 0; i < cr.meta.size(); ++i) {
if (cr.meta[i].nodeKind == NodeKind::Pointer64
&& cr.meta[i].foldCollapsed) {
ptrLine = i;
break;
}
}
QVERIFY2(ptrLine >= 0, "Could not find collapsed Pointer64 line in compose output");
// Simulate hover over the value column of the pointer line
const LineMeta& lm = cr.meta[ptrLine];
QString lineText;
{
long len = m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)ptrLine);
QByteArray buf(len + 1, '\0');
m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_GETLINE, (uintptr_t)ptrLine, static_cast<const char*>(buf.data()));
lineText = QString::fromUtf8(buf.left(len));
}
ColumnSpan vs = m_editor->valueSpan(lm, lineText.size(),
lm.effectiveTypeW, lm.effectiveNameW);
QVERIFY2(vs.valid, "Value span for pointer line is not valid");
int hoverCol = (vs.start + vs.end) / 2; // middle of value span
QPoint vp = colToViewport(m_editor->scintilla(), ptrLine, hoverCol);
sendMouseMove(m_editor->scintilla()->viewport(), vp);
QApplication::processEvents();
// Verify struct preview popup is shown
QVERIFY2(m_editor->structPreviewPopup() != nullptr,
"Struct preview popup was not created");
QVERIFY2(m_editor->structPreviewPopup()->isVisible(),
"Struct preview popup is not visible");
// Restore original document for other tests
m_editor->setProviderRef(nullptr, nullptr, nullptr);
m_editor->applyDocument(m_result);
}
void testStructPreviewPopupNotShownWhenExpanded() {
// Same tree but pointer is NOT collapsed — popup should not show
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "TestRoot";
root.name = "Root";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node target;
target.kind = NodeKind::Struct;
target.structTypeName = "TargetStruct";
target.name = "TargetStruct";
target.parentId = 0;
target.offset = 0;
int ti = tree.addNode(target);
uint64_t targetId = tree.nodes[ti].id;
{
Node f; f.parentId = targetId;
f.kind = NodeKind::UInt64; f.name = "FieldA"; f.offset = 0;
tree.addNode(f);
f.kind = NodeKind::UInt64; f.name = "FieldB"; f.offset = 8;
tree.addNode(f);
}
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "pTarget";
ptr.parentId = rootId;
ptr.offset = 0;
ptr.refId = targetId;
ptr.collapsed = false; // expanded
tree.addNode(ptr);
QByteArray data(64, '\0');
uint64_t ptrVal = 0x00007FFE12340000ULL;
memcpy(data.data(), &ptrVal, 8);
BufferProvider prov(data, "test_struct_preview_expanded");
ComposeResult cr = compose(tree, prov);
m_editor->applyDocument(cr);
m_editor->setProviderRef(&prov, nullptr, &tree);
QApplication::processEvents();
// Find the pointer line (should be Pointer64 and NOT collapsed)
int ptrLine = -1;
for (int i = 0; i < cr.meta.size(); ++i) {
if (cr.meta[i].nodeKind == NodeKind::Pointer64) {
ptrLine = i;
break;
}
}
QVERIFY2(ptrLine >= 0, "Could not find Pointer64 line in compose output");
// Hover at a middle column on the pointer line — expanded pointer header
// may not have a standard value span, but we just need to verify no popup
int hoverCol = 40; // somewhere in the middle of the line
QPoint vp = colToViewport(m_editor->scintilla(), ptrLine, hoverCol);
sendMouseMove(m_editor->scintilla()->viewport(), vp);
QApplication::processEvents();
// Struct preview popup should NOT be visible (pointer is expanded)
bool popupVisible = m_editor->structPreviewPopup()
&& m_editor->structPreviewPopup()->isVisible();
QVERIFY2(!popupVisible,
"Struct preview popup should not appear for expanded pointer");
// Restore
m_editor->setProviderRef(nullptr, nullptr, nullptr);
m_editor->applyDocument(m_result);
}
// ── Test: expanded pointer renders child fields from buffer ──
void testPointerExpansionRendersChildren() {
PtrDemo d = makePtrDemo(/*collapsed=*/false);
ComposeResult cr = compose(d.tree, d.prov);
m_editor->applyDocument(cr);
QApplication::processEvents();
// Find the pointer header line
int ptrHeaderLine = -1;
for (int i = 0; i < cr.meta.size(); ++i) {
if (cr.meta[i].nodeKind == NodeKind::Pointer64
&& cr.meta[i].foldHead && !cr.meta[i].foldCollapsed) {
ptrHeaderLine = i;
break;
}
}
QVERIFY2(ptrHeaderLine >= 0, "Should have an expanded Pointer64 header");
QCOMPARE(cr.meta[ptrHeaderLine].lineKind, LineKind::Header);
// Find expanded child fields (x, y, z at depth = header depth + 1)
int headerDepth = cr.meta[ptrHeaderLine].depth;
int childFieldCount = 0;
for (int i = ptrHeaderLine + 1; i < cr.meta.size(); ++i) {
const LineMeta& lm = cr.meta[i];
if (lm.depth == headerDepth + 1 && lm.lineKind == LineKind::Field)
childFieldCount++;
if (lm.lineKind == LineKind::Footer && lm.nodeKind == NodeKind::Pointer64)
break; // reached pointer footer
}
QCOMPARE(childFieldCount, 3); // x, y, z
// Find the pointer footer line
int ptrFooterLine = -1;
for (int i = ptrHeaderLine + 1; i < cr.meta.size(); ++i) {
if (cr.meta[i].lineKind == LineKind::Footer
&& cr.meta[i].nodeKind == NodeKind::Pointer64) {
ptrFooterLine = i;
break;
}
}
QVERIFY2(ptrFooterLine > ptrHeaderLine, "Should have a pointer footer after header");
// Verify the composed text contains the child field values
// UInt32 displays as hex (e.g. 100 → "0x00000064"), Float as decimal
QStringList lines = cr.text.split('\n');
bool foundX = false, foundY = false, foundZ = false;
for (const QString& line : lines) {
if (line.contains("0x64") && line.contains("x")) foundX = true; // 100 = 0x64
if (line.contains("0xc8") && line.contains("y")) foundY = true; // 200 = 0xc8
if (line.contains("3.14") && line.contains("z")) foundZ = true;
}
QVERIFY2(foundX, "Child field 'x' with value 0x64 should appear in output");
QVERIFY2(foundY, "Child field 'y' with value 0xc8 should appear in output");
QVERIFY2(foundZ, "Child field 'z' with value 3.14 should appear in output");
// Verify the pointer type name appears
QVERIFY2(cr.text.contains("ChildData*"),
"Pointer type 'ChildData*' should appear in output");
// Editor should have rendered all lines
int editorLineCount = m_editor->scintilla()->lines();
QVERIFY2(editorLineCount >= cr.meta.size(),
qPrintable(QString("Editor has %1 lines but compose has %2 meta entries")
.arg(editorLineCount).arg(cr.meta.size())));
m_editor->applyDocument(m_result);
}
// ── Test: collapsed pointer hides child fields ──
void testPointerCollapsedHidesChildren() {
PtrDemo expanded = makePtrDemo(/*collapsed=*/false);
ComposeResult crExpanded = compose(expanded.tree, expanded.prov);
PtrDemo collapsed = makePtrDemo(/*collapsed=*/true);
ComposeResult crCollapsed = compose(collapsed.tree, collapsed.prov);
// Collapsed should have fewer lines (no child fields, no pointer footer)
QVERIFY2(crCollapsed.meta.size() < crExpanded.meta.size(),
qPrintable(QString("Collapsed (%1 lines) should be smaller than expanded (%2)")
.arg(crCollapsed.meta.size()).arg(crExpanded.meta.size())));
// The pointer line should be a Field (not Header) with foldCollapsed=true
bool foundCollapsedPtr = false;
for (const LineMeta& lm : crCollapsed.meta) {
if (lm.nodeKind == NodeKind::Pointer64 && lm.foldHead) {
QVERIFY(lm.foldCollapsed);
QCOMPARE(lm.lineKind, LineKind::Field);
foundCollapsedPtr = true;
break;
}
}
QVERIFY2(foundCollapsedPtr, "Should have a collapsed Pointer64 fold head");
// No child fields from ChildData should appear in the main struct section
bool foundChildField = false;
for (const LineMeta& lm : crCollapsed.meta) {
if (lm.lineKind == LineKind::Footer && lm.nodeKind == NodeKind::Pointer64) {
foundChildField = true; // pointer footer exists = children visible
break;
}
}
QVERIFY2(!foundChildField,
"Collapsed pointer should not have a pointer footer (no children)");
// Apply collapsed to editor
m_editor->applyDocument(crCollapsed);
QApplication::processEvents();
int collapsedLines = m_editor->scintilla()->lines();
m_editor->applyDocument(crExpanded);
QApplication::processEvents();
int expandedLines = m_editor->scintilla()->lines();
QVERIFY2(collapsedLines < expandedLines,
qPrintable(QString("Collapsed (%1 editor lines) should be fewer than expanded (%2)")
.arg(collapsedLines).arg(expandedLines)));
m_editor->applyDocument(m_result);
}
// ── Test: null pointer still shows template fields (via NullProvider) ──
void testPointerNullShowsTemplate() {
PtrDemo d = makePtrDemo(/*collapsed=*/false, /*nullPtr=*/true);
ComposeResult cr = compose(d.tree, d.prov);
m_editor->applyDocument(cr);
QApplication::processEvents();
// Even with null pointer, expanded pointer should show template children
int ptrHeaderLine = -1;
for (int i = 0; i < cr.meta.size(); ++i) {
if (cr.meta[i].nodeKind == NodeKind::Pointer64
&& cr.meta[i].foldHead && !cr.meta[i].foldCollapsed) {
ptrHeaderLine = i;
break;
}
}
QVERIFY2(ptrHeaderLine >= 0,
"Null pointer should still produce an expanded header");
// Should have child field lines (template from NullProvider shows zeros)
int headerDepth = cr.meta[ptrHeaderLine].depth;
int childFieldCount = 0;
for (int i = ptrHeaderLine + 1; i < cr.meta.size(); ++i) {
const LineMeta& lm = cr.meta[i];
if (lm.depth == headerDepth + 1 && lm.lineKind == LineKind::Field)
childFieldCount++;
if (lm.lineKind == LineKind::Footer && lm.nodeKind == NodeKind::Pointer64)
break;
}
QCOMPARE(childFieldCount, 3); // x, y, z template still rendered
// Verify ChildData* appears in output
QVERIFY2(cr.text.contains("ChildData*"),
"Null pointer should still show 'ChildData*' type");
m_editor->applyDocument(m_result);
}
// ── Test: nested pointer chain renders multiple expansion levels ──
void testPointerChainExpansion() {
NodeTree tree;
tree.baseAddress = 0;
// Root struct
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "Chain";
root.name = "chain";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Inner struct (innermost target)
Node inner;
inner.kind = NodeKind::Struct;
inner.structTypeName = "Inner";
inner.name = "Inner";
inner.parentId = 0;
inner.offset = 300;
int ii = tree.addNode(inner);
uint64_t innerId = tree.nodes[ii].id;
{
Node f;
f.kind = NodeKind::UInt32; f.name = "value";
f.parentId = innerId; f.offset = 0;
tree.addNode(f);
}
// Outer struct (contains pointer to Inner)
Node outer;
outer.kind = NodeKind::Struct;
outer.structTypeName = "Outer";
outer.name = "Outer";
outer.parentId = 0;
outer.offset = 200;
int oi = tree.addNode(outer);
uint64_t outerId = tree.nodes[oi].id;
{
Node f;
f.kind = NodeKind::UInt32; f.name = "tag";
f.parentId = outerId; f.offset = 0;
tree.addNode(f);
Node p;
p.kind = NodeKind::Pointer64; p.name = "pInner";
p.parentId = outerId; p.offset = 8;
p.refId = innerId;
tree.addNode(p);
}
// Root pointer to Outer
{
Node p;
p.kind = NodeKind::Pointer64; p.name = "pOuter";
p.parentId = rootId; p.offset = 0;
p.refId = outerId;
tree.addNode(p);
}
// Buffer: pOuter at 0 → 32, pInner at 32+8=40 → 64, value at 64 = 999
QByteArray data(128, '\0');
uint64_t pOuter = 32; memcpy(data.data() + 0, &pOuter, 8);
uint64_t pInner = 64; memcpy(data.data() + 40, &pInner, 8);
uint32_t tag = 0xAB; memcpy(data.data() + 32, &tag, 4);
uint32_t val = 999; memcpy(data.data() + 64, &val, 4);
BufferProvider prov(data, "chain_demo");
ComposeResult cr = compose(tree, prov);
m_editor->applyDocument(cr);
QApplication::processEvents();
// Both Outer* and Inner* should appear
QVERIFY2(cr.text.contains("Outer*"), "Should display 'Outer*' pointer type");
QVERIFY2(cr.text.contains("Inner*"), "Should display 'Inner*' pointer type");
// Count pointer fold heads — should have at least 2 (pOuter + pInner)
int ptrFoldHeads = 0;
int maxDepth = 0;
for (const LineMeta& lm : cr.meta) {
if (lm.foldHead && lm.nodeKind == NodeKind::Pointer64)
ptrFoldHeads++;
if (lm.depth > maxDepth) maxDepth = lm.depth;
}
QVERIFY2(ptrFoldHeads >= 2,
qPrintable(QString("Expected >=2 pointer fold heads, got %1")
.arg(ptrFoldHeads)));
// Depth should reach at least 3 (root=0, pOuter children=1..2, pInner children=2..3)
QVERIFY2(maxDepth >= 3,
qPrintable(QString("Expected max depth >= 3 for chain, got %1")
.arg(maxDepth)));
// Verify innermost value (999 = 0x3e7) appears in the output
QVERIFY2(cr.text.contains("0x3e7"),
"Innermost field 'value = 0x3e7' should appear in chain expansion");
m_editor->applyDocument(m_result);
}
};
QTEST_MAIN(TestEditor)

View File

@@ -21,7 +21,7 @@ Q_DECLARE_METATYPE(rcx::TypeEntry)
using namespace rcx;
static void buildTwoRootTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node a;
a.kind = NodeKind::Struct;

View File

@@ -16,7 +16,7 @@ using namespace rcx;
// ── Fixture: small tree with diverse field types ──
static void buildValidationTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;

View File

@@ -260,17 +260,6 @@ private slots:
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
}
void provider_setBase()
{
WinDbgMemoryProvider prov(m_connString);
QVERIFY(prov.isValid());
uint64_t orig = prov.base();
prov.setBase(0x1000);
QCOMPARE(prov.base(), (uint64_t)0x1000);
prov.setBase(orig);
QCOMPARE(prov.base(), orig);
}
// ── Read: MZ header on main thread ──
void provider_read_mz_mainThread()