mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: switch provider addressing from RVA to absolute, add pointer expansion tests
This commit is contained in:
@@ -65,7 +65,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
|||||||
if (!m_handle || len <= 0) return false;
|
if (!m_handle || len <= 0) return false;
|
||||||
|
|
||||||
SIZE_T bytesRead = 0;
|
SIZE_T bytesRead = 0;
|
||||||
ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead);
|
ReadProcessMemory(m_handle, (LPCVOID)(addr), buf, (SIZE_T)len, &bytesRead);
|
||||||
if ((int)bytesRead < len)
|
if ((int)bytesRead < len)
|
||||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||||
return bytesRead > 0;
|
return bytesRead > 0;
|
||||||
@@ -76,7 +76,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
|||||||
if (!m_handle || !m_writable || len <= 0) return false;
|
if (!m_handle || !m_writable || len <= 0) return false;
|
||||||
|
|
||||||
SIZE_T bytesWritten = 0;
|
SIZE_T bytesWritten = 0;
|
||||||
if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten))
|
if (WriteProcessMemory(m_handle, (LPVOID)(addr), buf, (SIZE_T)len, &bytesWritten))
|
||||||
return bytesWritten == (SIZE_T)len;
|
return bytesWritten == (SIZE_T)len;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -156,15 +156,13 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
|||||||
{
|
{
|
||||||
if (m_fd < 0 || len <= 0) return false;
|
if (m_fd < 0 || len <= 0) return false;
|
||||||
|
|
||||||
uint64_t absAddr = m_base + addr;
|
|
||||||
|
|
||||||
// Try process_vm_readv first (faster, no fd seek contention)
|
// Try process_vm_readv first (faster, no fd seek contention)
|
||||||
struct iovec local;
|
struct iovec local;
|
||||||
local.iov_base = buf;
|
local.iov_base = buf;
|
||||||
local.iov_len = static_cast<size_t>(len);
|
local.iov_len = static_cast<size_t>(len);
|
||||||
|
|
||||||
struct iovec remote;
|
struct iovec remote;
|
||||||
remote.iov_base = reinterpret_cast<void*>(absAddr);
|
remote.iov_base = reinterpret_cast<void*>(addr);
|
||||||
remote.iov_len = static_cast<size_t>(len);
|
remote.iov_len = static_cast<size_t>(len);
|
||||||
|
|
||||||
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
|
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
|
||||||
@@ -172,7 +170,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Fallback: pread on /proc/<pid>/mem
|
// Fallback: pread on /proc/<pid>/mem
|
||||||
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
|
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
|
||||||
return nread == static_cast<ssize_t>(len);
|
return nread == static_cast<ssize_t>(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,15 +178,13 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
|||||||
{
|
{
|
||||||
if (m_fd < 0 || !m_writable || len <= 0) return false;
|
if (m_fd < 0 || !m_writable || len <= 0) return false;
|
||||||
|
|
||||||
uint64_t absAddr = m_base + addr;
|
|
||||||
|
|
||||||
// Try process_vm_writev first
|
// Try process_vm_writev first
|
||||||
struct iovec local;
|
struct iovec local;
|
||||||
local.iov_base = const_cast<void*>(buf);
|
local.iov_base = const_cast<void*>(buf);
|
||||||
local.iov_len = static_cast<size_t>(len);
|
local.iov_len = static_cast<size_t>(len);
|
||||||
|
|
||||||
struct iovec remote;
|
struct iovec remote;
|
||||||
remote.iov_base = reinterpret_cast<void*>(absAddr);
|
remote.iov_base = reinterpret_cast<void*>(addr);
|
||||||
remote.iov_len = static_cast<size_t>(len);
|
remote.iov_len = static_cast<size_t>(len);
|
||||||
|
|
||||||
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
|
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
|
||||||
@@ -196,7 +192,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Fallback: pwrite on /proc/<pid>/mem
|
// Fallback: pwrite on /proc/<pid>/mem
|
||||||
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
|
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
|
||||||
return nwritten == static_cast<ssize_t>(len);
|
return nwritten == static_cast<ssize_t>(len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,16 @@ public:
|
|||||||
|
|
||||||
bool isLive() const override { return true; }
|
bool isLive() const override { return true; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
void setBase(uint64_t b) override { m_base = b; }
|
bool isReadable(uint64_t, int len) const override {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return m_handle && len >= 0;
|
||||||
|
#elif defined(__linux__)
|
||||||
|
return m_fd >= 0 && len >= 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// Process-specific helpers
|
// Process-specific helpers
|
||||||
uint32_t pid() const { return m_pid; }
|
uint32_t pid() const { return m_pid; }
|
||||||
uint64_t baseAddress() const { return m_base; }
|
|
||||||
void refreshModules() { m_modules.clear(); cacheModules(); }
|
void refreshModules() { m_modules.clear(); cacheModules(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -33,9 +33,8 @@ bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const
|
|||||||
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
|
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint64_t absAddr = m_base + addr;
|
|
||||||
return m_fns.ReadRemoteMemory(m_handle,
|
return m_fns.ReadRemoteMemory(m_handle,
|
||||||
reinterpret_cast<RC_Pointer>(absAddr),
|
reinterpret_cast<RC_Pointer>(addr),
|
||||||
static_cast<RC_Pointer>(buf),
|
static_cast<RC_Pointer>(buf),
|
||||||
0, len);
|
0, len);
|
||||||
}
|
}
|
||||||
@@ -54,9 +53,8 @@ bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len)
|
|||||||
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
|
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint64_t absAddr = m_base + addr;
|
|
||||||
return m_fns.WriteRemoteMemory(m_handle,
|
return m_fns.WriteRemoteMemory(m_handle,
|
||||||
reinterpret_cast<RC_Pointer>(absAddr),
|
reinterpret_cast<RC_Pointer>(addr),
|
||||||
const_cast<RC_Pointer>(static_cast<const void*>(buf)),
|
const_cast<RC_Pointer>(static_cast<const void*>(buf)),
|
||||||
0, len);
|
0, len);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public:
|
|||||||
QString kind() const override { return QStringLiteral("RcNet"); }
|
QString kind() const override { return QStringLiteral("RcNet"); }
|
||||||
bool isLive() const override { return true; }
|
bool isLive() const override { return true; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
void setBase(uint64_t b) override { m_base = b; }
|
|
||||||
QString getSymbol(uint64_t addr) const override;
|
QString getSymbol(uint64_t addr) const override;
|
||||||
|
|
||||||
struct ModuleInfo {
|
struct ModuleInfo {
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
|||||||
bool result = false;
|
bool result = false;
|
||||||
dispatchToOwner([&]() {
|
dispatchToOwner([&]() {
|
||||||
ULONG bytesRead = 0;
|
ULONG bytesRead = 0;
|
||||||
HRESULT hr = m_dataSpaces->ReadVirtual(m_base + addr, buf, (ULONG)len, &bytesRead);
|
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
||||||
if (FAILED(hr) || (int)bytesRead < len)
|
if (FAILED(hr) || (int)bytesRead < len)
|
||||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||||
result = bytesRead > 0;
|
result = bytesRead > 0;
|
||||||
@@ -324,7 +324,7 @@ bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
|||||||
bool result = false;
|
bool result = false;
|
||||||
dispatchToOwner([&]() {
|
dispatchToOwner([&]() {
|
||||||
ULONG bytesWritten = 0;
|
ULONG bytesWritten = 0;
|
||||||
HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast<void*>(buf),
|
HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
|
||||||
(ULONG)len, &bytesWritten);
|
(ULONG)len, &bytesWritten);
|
||||||
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
|
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
|
||||||
});
|
});
|
||||||
@@ -364,7 +364,7 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
|
|||||||
char nameBuf[512] = {};
|
char nameBuf[512] = {};
|
||||||
ULONG nameSize = 0;
|
ULONG nameSize = 0;
|
||||||
ULONG64 displacement = 0;
|
ULONG64 displacement = 0;
|
||||||
HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf),
|
HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
|
||||||
&nameSize, &displacement);
|
&nameSize, &displacement);
|
||||||
if (SUCCEEDED(hr) && nameSize > 0) {
|
if (SUCCEEDED(hr) && nameSize > 0) {
|
||||||
result = QString::fromUtf8(nameBuf);
|
result = QString::fromUtf8(nameBuf);
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ public:
|
|||||||
|
|
||||||
bool isLive() const override { return m_isLive; }
|
bool isLive() const override { return m_isLive; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
void setBase(uint64_t b) override { m_base = b; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
||||||
|
|||||||
@@ -78,12 +78,6 @@ static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
|
|||||||
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
|
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
|
|
||||||
if (tree.baseAddress == 0) return ptr;
|
|
||||||
if (ptr >= tree.baseAddress) return ptr - tree.baseAddress;
|
|
||||||
return UINT64_MAX; // Invalid: ptr below base address
|
|
||||||
}
|
|
||||||
|
|
||||||
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
|
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
|
||||||
int64_t total = 0;
|
int64_t total = 0;
|
||||||
QSet<uint64_t> visited;
|
QSet<uint64_t> visited;
|
||||||
@@ -140,8 +134,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.isContinuation = isCont;
|
lm.isContinuation = isCont;
|
||||||
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
lm.offsetAddr = absAddr;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
@@ -187,8 +181,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::Field;
|
lm.lineKind = LineKind::Field;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
lm.offsetAddr = absAddr;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||||
@@ -206,8 +200,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::ArrayElementSeparator;
|
lm.lineKind = LineKind::ArrayElementSeparator;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
lm.offsetAddr = absAddr;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
@@ -236,8 +230,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::Header;
|
lm.lineKind = LineKind::Header;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
lm.offsetAddr = absAddr;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.isRootHeader = false;
|
lm.isRootHeader = false;
|
||||||
@@ -300,8 +294,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.lineKind = LineKind::Field;
|
lm.lineKind = LineKind::Field;
|
||||||
lm.nodeKind = node.elementKind;
|
lm.nodeKind = node.elementKind;
|
||||||
lm.isArrayElement = true;
|
lm.isArrayElement = true;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + elemAddr;
|
lm.offsetAddr = elemAddr;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
||||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||||
@@ -353,9 +347,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.depth = childDepth;
|
lm.depth = childDepth;
|
||||||
lm.lineKind = LineKind::Header;
|
lm.lineKind = LineKind::Header;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(
|
lm.offsetText = fmt::fmtOffsetMargin(
|
||||||
tree.baseAddress + absAddr + child.offset, false,
|
absAddr + child.offset, false,
|
||||||
state.offsetHexDigits);
|
state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
|
lm.offsetAddr = absAddr + child.offset;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.nodeKind = child.kind;
|
lm.nodeKind = child.kind;
|
||||||
lm.foldHead = true;
|
lm.foldHead = true;
|
||||||
@@ -399,8 +393,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
lm.markerMask = 0;
|
lm.markerMask = 0;
|
||||||
int sz = tree.structSpan(node.id, &state.childMap);
|
int sz = tree.structSpan(node.id, &state.childMap);
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr + sz;
|
lm.offsetAddr = absAddr + sz;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||||
}
|
}
|
||||||
@@ -445,8 +439,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
lm.offsetAddr = absAddr;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldHead = true;
|
lm.foldHead = true;
|
||||||
@@ -472,26 +466,21 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
// Treat sentinel values as invalid pointers
|
// Treat sentinel values as invalid pointers
|
||||||
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
|
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
|
||||||
ptrVal = 0;
|
ptrVal = 0;
|
||||||
else {
|
|
||||||
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
|
|
||||||
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if pointer target is actually readable
|
// Pointer target address is used directly (absolute)
|
||||||
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
|
uint64_t pBase = ptrVal;
|
||||||
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
|
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
|
||||||
|
|
||||||
// For invalid/unreadable pointers: use NullProvider (shows zeros)
|
// For invalid/unreadable pointers: use NullProvider (shows zeros)
|
||||||
// and reset margin offsets (unsigned wrap cancels baseAddress)
|
|
||||||
static NullProvider s_nullProv;
|
static NullProvider s_nullProv;
|
||||||
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
|
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
|
||||||
if (!ptrReadable)
|
if (!ptrReadable)
|
||||||
pBase = (uint64_t)0 - tree.baseAddress;
|
pBase = 0;
|
||||||
|
|
||||||
uint64_t savedPtrBase = state.currentPtrBase;
|
uint64_t savedPtrBase = state.currentPtrBase;
|
||||||
state.currentPtrBase = tree.baseAddress + pBase;
|
state.currentPtrBase = pBase;
|
||||||
|
|
||||||
if (hasMaterialized) {
|
if (hasMaterialized) {
|
||||||
// Render materialized children at the pointer target address.
|
// Render materialized children at the pointer target address.
|
||||||
@@ -566,16 +555,16 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
state.childMap[tree.nodes[i].parentId].append(i);
|
state.childMap[tree.nodes[i].parentId].append(i);
|
||||||
|
|
||||||
// Precompute absolute offsets
|
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
||||||
state.absOffsets.resize(tree.nodes.size());
|
state.absOffsets.resize(tree.nodes.size());
|
||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
state.absOffsets[i] = tree.computeOffset(i);
|
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
|
||||||
|
|
||||||
// Compute hex digit tier from max absolute address
|
// Compute hex digit tier from max absolute address
|
||||||
{
|
{
|
||||||
uint64_t maxAddr = tree.baseAddress;
|
uint64_t maxAddr = tree.baseAddress;
|
||||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
uint64_t addr = tree.baseAddress + (uint64_t)state.absOffsets[i];
|
uint64_t addr = (uint64_t)state.absOffsets[i];
|
||||||
if (addr > maxAddr) maxAddr = addr;
|
if (addr > maxAddr) maxAddr = addr;
|
||||||
}
|
}
|
||||||
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;
|
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;
|
||||||
|
|||||||
@@ -451,8 +451,6 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
m_doc->dataPath.clear();
|
m_doc->dataPath.clear();
|
||||||
if (m_doc->tree.baseAddress == 0)
|
if (m_doc->tree.baseAddress == 0)
|
||||||
m_doc->tree.baseAddress = newBase;
|
m_doc->tree.baseAddress = newBase;
|
||||||
else
|
|
||||||
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
|
||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
emit m_doc->documentChanged();
|
emit m_doc->documentChanged();
|
||||||
|
|
||||||
@@ -672,10 +670,7 @@ void RcxController::refresh() {
|
|||||||
if (isFuncPtr(node.kind)) continue;
|
if (isFuncPtr(node.kind)) continue;
|
||||||
|
|
||||||
// Use the absolute address from compose (correct for pointer-expanded nodes)
|
// Use the absolute address from compose (correct for pointer-expanded nodes)
|
||||||
// and convert to provider-relative by subtracting the base address.
|
uint64_t addr = lm.offsetAddr;
|
||||||
uint64_t addr = lm.offsetAddr >= m_doc->tree.baseAddress
|
|
||||||
? lm.offsetAddr - m_doc->tree.baseAddress
|
|
||||||
: static_cast<uint64_t>(m_doc->tree.computeOffset(lm.nodeIdx));
|
|
||||||
int sz = node.byteSize();
|
int sz = node.byteSize();
|
||||||
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
|
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
|
||||||
|
|
||||||
@@ -1039,12 +1034,6 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
clearHistoryForAdjs(c.offAdjs);
|
clearHistoryForAdjs(c.offAdjs);
|
||||||
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
||||||
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
||||||
qDebug() << "[ChangeBase] tree.baseAddress =" << Qt::hex << tree.baseAddress
|
|
||||||
<< "provider =" << (m_doc->provider ? "yes" : "null");
|
|
||||||
if (m_doc->provider) {
|
|
||||||
m_doc->provider->setBase(tree.baseAddress);
|
|
||||||
qDebug() << "[ChangeBase] provider->base() now =" << Qt::hex << m_doc->provider->base();
|
|
||||||
}
|
|
||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
||||||
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
||||||
@@ -1103,7 +1092,7 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
|
|||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
|
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
|
||||||
if (signedAddr < 0) return; // malformed tree: negative offset
|
if (signedAddr < 0) return; // malformed tree: negative offset
|
||||||
uint64_t addr = static_cast<uint64_t>(signedAddr);
|
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedAddr);
|
||||||
|
|
||||||
// For vector components, redirect to float parsing at sub-offset
|
// For vector components, redirect to float parsing at sub-offset
|
||||||
NodeKind editKind = node.kind;
|
NodeKind editKind = node.kind;
|
||||||
@@ -2072,8 +2061,6 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
|||||||
m_doc->dataPath.clear();
|
m_doc->dataPath.clear();
|
||||||
if (m_doc->tree.baseAddress == 0)
|
if (m_doc->tree.baseAddress == 0)
|
||||||
m_doc->tree.baseAddress = newBase;
|
m_doc->tree.baseAddress = newBase;
|
||||||
else
|
|
||||||
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
|
||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
emit m_doc->documentChanged();
|
emit m_doc->documentChanged();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -2134,7 +2121,7 @@ void RcxController::setupAutoRefresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recursively collect memory ranges for a struct and its pointer targets.
|
// Recursively collect memory ranges for a struct and its pointer targets.
|
||||||
// memBase is the provider-relative address where this struct's data lives.
|
// memBase is the absolute address where this struct's data lives.
|
||||||
void RcxController::collectPointerRanges(
|
void RcxController::collectPointerRanges(
|
||||||
uint64_t structId, uint64_t memBase,
|
uint64_t structId, uint64_t memBase,
|
||||||
int depth, int maxDepth,
|
int depth, int maxDepth,
|
||||||
@@ -2167,9 +2154,9 @@ void RcxController::collectPointerRanges(
|
|||||||
uint64_t ptrVal = (child.kind == NodeKind::Pointer32)
|
uint64_t ptrVal = (child.kind == NodeKind::Pointer32)
|
||||||
? (uint64_t)m_snapshotProv->readU32(ptrAddr)
|
? (uint64_t)m_snapshotProv->readU32(ptrAddr)
|
||||||
: m_snapshotProv->readU64(ptrAddr);
|
: m_snapshotProv->readU64(ptrAddr);
|
||||||
if (ptrVal == 0 || ptrVal == UINT64_MAX || ptrVal < m_doc->tree.baseAddress) continue;
|
if (ptrVal == 0 || ptrVal == UINT64_MAX) continue;
|
||||||
|
|
||||||
uint64_t pBase = ptrVal - m_doc->tree.baseAddress;
|
uint64_t pBase = ptrVal;
|
||||||
collectPointerRanges(child.refId, pBase, depth + 1, maxDepth,
|
collectPointerRanges(child.refId, pBase, depth + 1, maxDepth,
|
||||||
visited, ranges);
|
visited, ranges);
|
||||||
}
|
}
|
||||||
@@ -2194,16 +2181,16 @@ void RcxController::onRefreshTick() {
|
|||||||
int extent = computeDataExtent();
|
int extent = computeDataExtent();
|
||||||
if (extent <= 0) return;
|
if (extent <= 0) return;
|
||||||
|
|
||||||
// Collect all needed ranges: main struct + pointer targets
|
// Collect all needed ranges: main struct + pointer targets (absolute addresses)
|
||||||
QVector<QPair<uint64_t,int>> ranges;
|
QVector<QPair<uint64_t,int>> ranges;
|
||||||
ranges.append({0, extent});
|
ranges.append({m_doc->tree.baseAddress, extent});
|
||||||
|
|
||||||
if (m_snapshotProv) {
|
if (m_snapshotProv) {
|
||||||
QSet<QPair<uint64_t,uint64_t>> visited;
|
QSet<QPair<uint64_t,uint64_t>> visited;
|
||||||
uint64_t rootId = m_viewRootId;
|
uint64_t rootId = m_viewRootId;
|
||||||
if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
|
if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
|
||||||
rootId = m_doc->tree.nodes[0].id;
|
rootId = m_doc->tree.nodes[0].id;
|
||||||
collectPointerRanges(rootId, 0, 0, 99, visited, ranges);
|
collectPointerRanges(rootId, m_doc->tree.baseAddress, 0, 99, visited, ranges);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_readInFlight = true;
|
m_readInFlight = true;
|
||||||
|
|||||||
184
src/editor.cpp
184
src/editor.cpp
@@ -255,6 +255,103 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class StructPreviewPopup : public QFrame {
|
||||||
|
uint64_t m_nodeId = 0;
|
||||||
|
QString m_body;
|
||||||
|
QLabel* m_titleLabel = nullptr;
|
||||||
|
QLabel* m_bodyLabel = nullptr;
|
||||||
|
public:
|
||||||
|
explicit StructPreviewPopup(QWidget* parent)
|
||||||
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||||
|
setFrameShape(QFrame::NoFrame);
|
||||||
|
setAutoFillBackground(true);
|
||||||
|
|
||||||
|
auto* vbox = new QVBoxLayout(this);
|
||||||
|
vbox->setContentsMargins(8, 6, 8, 6);
|
||||||
|
vbox->setSpacing(2);
|
||||||
|
|
||||||
|
m_titleLabel = new QLabel;
|
||||||
|
QFont bold = m_titleLabel->font();
|
||||||
|
bold.setBold(true);
|
||||||
|
m_titleLabel->setFont(bold);
|
||||||
|
vbox->addWidget(m_titleLabel);
|
||||||
|
|
||||||
|
auto* sep = new QFrame;
|
||||||
|
sep->setFrameShape(QFrame::HLine);
|
||||||
|
sep->setFrameShadow(QFrame::Plain);
|
||||||
|
sep->setFixedHeight(1);
|
||||||
|
vbox->addWidget(sep);
|
||||||
|
|
||||||
|
m_bodyLabel = new QLabel;
|
||||||
|
m_bodyLabel->setTextFormat(Qt::PlainText);
|
||||||
|
m_bodyLabel->setWordWrap(false);
|
||||||
|
vbox->addWidget(m_bodyLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t nodeId() const { return m_nodeId; }
|
||||||
|
|
||||||
|
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
||||||
|
const QFont& font) {
|
||||||
|
if (nodeId == m_nodeId && body == m_body && isVisible())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_nodeId = nodeId;
|
||||||
|
m_body = body;
|
||||||
|
|
||||||
|
const auto& theme = ThemeManager::instance().current();
|
||||||
|
QPalette pal;
|
||||||
|
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||||
|
pal.setColor(QPalette::WindowText, theme.text);
|
||||||
|
setPalette(pal);
|
||||||
|
|
||||||
|
QFont bold = font;
|
||||||
|
bold.setBold(true);
|
||||||
|
m_titleLabel->setFont(bold);
|
||||||
|
m_titleLabel->setText(title);
|
||||||
|
m_titleLabel->setStyleSheet(
|
||||||
|
QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||||
|
|
||||||
|
for (auto* child : findChildren<QFrame*>()) {
|
||||||
|
if (child->frameShape() == QFrame::HLine) {
|
||||||
|
QPalette sp;
|
||||||
|
sp.setColor(QPalette::WindowText, theme.border);
|
||||||
|
child->setPalette(sp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_bodyLabel->setFont(font);
|
||||||
|
m_bodyLabel->setText(body);
|
||||||
|
m_bodyLabel->setStyleSheet(
|
||||||
|
QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||||
|
|
||||||
|
setMaximumWidth(600);
|
||||||
|
adjustSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAt(const QPoint& globalPos) {
|
||||||
|
QSize sz = sizeHint();
|
||||||
|
QRect screen = QApplication::screenAt(globalPos)
|
||||||
|
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||||
|
: QRect(0, 0, 1920, 1080);
|
||||||
|
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||||
|
int y = globalPos.y();
|
||||||
|
if (y + sz.height() > screen.bottom())
|
||||||
|
y = globalPos.y() - sz.height() - 4;
|
||||||
|
move(x, y);
|
||||||
|
if (!isVisible()) show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismiss() {
|
||||||
|
if (isVisible()) hide();
|
||||||
|
m_nodeId = 0;
|
||||||
|
m_body.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static constexpr int IND_EDITABLE = 8;
|
static constexpr int IND_EDITABLE = 8;
|
||||||
static constexpr int IND_HEX_DIM = 9;
|
static constexpr int IND_HEX_DIM = 9;
|
||||||
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
||||||
@@ -2012,9 +2109,11 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
m_hoveredNodeId = 0;
|
m_hoveredNodeId = 0;
|
||||||
m_hoveredLine = -1;
|
m_hoveredLine = -1;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
// Dismiss hover popup so it gets recreated with Set buttons once edit starts
|
// Dismiss hover popups so they get recreated with Set buttons once edit starts
|
||||||
if (m_historyPopup)
|
if (m_historyPopup)
|
||||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
|
if (m_structPreviewPopup)
|
||||||
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||||
// Clear editable-token color hints (de-emphasize non-active tokens)
|
// Clear editable-token color hints (de-emphasize non-active tokens)
|
||||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||||
m_hintLine = -1;
|
m_hintLine = -1;
|
||||||
@@ -2580,9 +2679,11 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
}
|
}
|
||||||
// Always dismiss disasm popup during inline editing
|
// Always dismiss disasm/preview popups during inline editing
|
||||||
if (m_disasmPopup && m_disasmPopup->isVisible())
|
if (m_disasmPopup && m_disasmPopup->isVisible())
|
||||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||||
|
if (m_structPreviewPopup && m_structPreviewPopup->isVisible())
|
||||||
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2593,6 +2694,8 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
if (m_disasmPopup && !m_applyingDocument)
|
if (m_disasmPopup && !m_applyingDocument)
|
||||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||||
|
if (m_structPreviewPopup && !m_applyingDocument)
|
||||||
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2755,11 +2858,8 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
if (!isVoidPtr || node.refId == 0) {
|
if (!isVoidPtr || node.refId == 0) {
|
||||||
bool is64 = (lm.nodeKind == NodeKind::FuncPtr64
|
bool is64 = (lm.nodeKind == NodeKind::FuncPtr64
|
||||||
|| lm.nodeKind == NodeKind::Pointer64);
|
|| lm.nodeKind == NodeKind::Pointer64);
|
||||||
// Use composed address (correct for pointer-expanded nodes)
|
// Use composed address (absolute, correct for pointer-expanded nodes)
|
||||||
// not node.offset (which is just offset within struct definition).
|
uint64_t provAddr = lm.offsetAddr;
|
||||||
uint64_t provAddr = lm.offsetAddr >= m_disasmTree->baseAddress
|
|
||||||
? lm.offsetAddr - m_disasmTree->baseAddress
|
|
||||||
: static_cast<uint64_t>(node.offset);
|
|
||||||
uint64_t ptrVal = is64
|
uint64_t ptrVal = is64
|
||||||
? m_disasmProvider->readU64(provAddr)
|
? m_disasmProvider->readU64(provAddr)
|
||||||
: (uint64_t)m_disasmProvider->readU32(provAddr);
|
: (uint64_t)m_disasmProvider->readU32(provAddr);
|
||||||
@@ -2768,13 +2868,11 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
// Read code bytes from the function target address.
|
// Read code bytes from the function target address.
|
||||||
// Use the real provider (not snapshot) because function
|
// Use the real provider (not snapshot) because function
|
||||||
// code lives at arbitrary process addresses that aren't
|
// code lives at arbitrary process addresses that aren't
|
||||||
// in the snapshot page table. The provider reads from
|
// in the snapshot page table.
|
||||||
// m_base + addr via ReadProcessMemory, so we convert
|
|
||||||
// the absolute ptrVal to provider-relative.
|
|
||||||
const Provider* codeProv = m_disasmRealProv
|
const Provider* codeProv = m_disasmRealProv
|
||||||
? m_disasmRealProv : m_disasmProvider;
|
? m_disasmRealProv : m_disasmProvider;
|
||||||
constexpr int kMaxRead = 128;
|
constexpr int kMaxRead = 128;
|
||||||
uint64_t codeAddr = ptrVal - m_disasmTree->baseAddress;
|
uint64_t codeAddr = ptrVal;
|
||||||
QByteArray bytes(kMaxRead, Qt::Uninitialized);
|
QByteArray bytes(kMaxRead, Qt::Uninitialized);
|
||||||
bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead);
|
bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead);
|
||||||
if (readOk) {
|
if (readOk) {
|
||||||
@@ -2837,6 +2935,70 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Struct preview popup for collapsed typed pointers
|
||||||
|
{
|
||||||
|
bool showPreview = false;
|
||||||
|
if (m_disasmTree && m_disasmProvider && h.line >= 0 && h.line < m_meta.size()) {
|
||||||
|
const LineMeta& lm = m_meta[h.line];
|
||||||
|
bool isTypedPtr = (lm.nodeKind == NodeKind::Pointer32
|
||||||
|
|| lm.nodeKind == NodeKind::Pointer64)
|
||||||
|
&& !lm.pointerTargetName.isEmpty();
|
||||||
|
if (isTypedPtr && lm.foldCollapsed
|
||||||
|
&& lm.nodeIdx >= 0 && lm.nodeIdx < m_disasmTree->nodes.size()) {
|
||||||
|
const Node& node = m_disasmTree->nodes[lm.nodeIdx];
|
||||||
|
if (node.refId != 0) {
|
||||||
|
QString lineText = getLineText(m_sci, h.line);
|
||||||
|
ColumnSpan vs = narrowPtrValueSpan(lm,
|
||||||
|
valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW),
|
||||||
|
lineText);
|
||||||
|
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
||||||
|
ComposeResult cr = rcx::compose(*m_disasmTree, *m_disasmProvider, node.refId);
|
||||||
|
// Skip command row (line 0), take first 5 data lines
|
||||||
|
QStringList lines = cr.text.split('\n');
|
||||||
|
constexpr int kMaxLines = 5;
|
||||||
|
QString body;
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 1; i < lines.size() && count < kMaxLines; ++i) {
|
||||||
|
if (!lines[i].isEmpty()) {
|
||||||
|
if (count > 0) body += '\n';
|
||||||
|
body += lines[i];
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!body.isEmpty()) {
|
||||||
|
if (!m_structPreviewPopup)
|
||||||
|
m_structPreviewPopup = new StructPreviewPopup(this);
|
||||||
|
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup);
|
||||||
|
popup->populate(lm.nodeId,
|
||||||
|
lm.pointerTargetName, body, editorFont());
|
||||||
|
long linePos = m_sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||||
|
(unsigned long)h.line);
|
||||||
|
long byteOff = lineText.left(vs.start).toUtf8().size();
|
||||||
|
int px = (int)m_sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||||
|
(unsigned long)0, linePos + byteOff);
|
||||||
|
int py = (int)m_sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
||||||
|
(unsigned long)0, linePos);
|
||||||
|
int lh = (int)m_sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_TEXTHEIGHT,
|
||||||
|
(unsigned long)h.line);
|
||||||
|
QPoint anchor = m_sci->viewport()->mapToGlobal(
|
||||||
|
QPoint(px, py + lh));
|
||||||
|
popup->showAt(anchor);
|
||||||
|
showPreview = true;
|
||||||
|
if (m_historyPopup && m_historyPopup->isVisible())
|
||||||
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!showPreview && m_structPreviewPopup && m_structPreviewPopup->isVisible())
|
||||||
|
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
// Determine cursor shape based on interaction type
|
// Determine cursor shape based on interaction type
|
||||||
Qt::CursorShape desired = Qt::ArrowCursor;
|
Qt::CursorShape desired = Qt::ArrowCursor;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public:
|
|||||||
void restoreViewState(const ViewState& vs);
|
void restoreViewState(const ViewState& vs);
|
||||||
|
|
||||||
QsciScintilla* scintilla() const { return m_sci; }
|
QsciScintilla* scintilla() const { return m_sci; }
|
||||||
|
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
|
||||||
const LineMeta* metaForLine(int line) const;
|
const LineMeta* metaForLine(int line) const;
|
||||||
int currentNodeIndex() const;
|
int currentNodeIndex() const;
|
||||||
void scrollToNodeId(uint64_t nodeId);
|
void scrollToNodeId(uint64_t nodeId);
|
||||||
@@ -138,6 +139,7 @@ private:
|
|||||||
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||||
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
|
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
|
||||||
|
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
|
||||||
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
||||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||||
const NodeTree* m_disasmTree = nullptr;
|
const NodeTree* m_disasmTree = nullptr;
|
||||||
|
|||||||
@@ -287,7 +287,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
{"name", "hex.read"},
|
{"name", "hex.read"},
|
||||||
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
|
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
|
||||||
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
|
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
|
||||||
"Offset is provider-relative (0-based) unless baseRelative=true."},
|
"Offset is tree-relative (0-based, baseAddress added automatically) "
|
||||||
|
"unless baseRelative=true (offset is absolute)."},
|
||||||
{"inputSchema", QJsonObject{
|
{"inputSchema", QJsonObject{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", QJsonObject{
|
{"properties", QJsonObject{
|
||||||
@@ -825,8 +826,8 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
|
|||||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||||
int length = qMin(args.value("length").toInt(64), 4096);
|
int length = qMin(args.value("length").toInt(64), 4096);
|
||||||
|
|
||||||
if (args.value("baseRelative").toBool())
|
if (!args.value("baseRelative").toBool())
|
||||||
offset -= (int64_t)tab->doc->tree.baseAddress;
|
offset += (int64_t)tab->doc->tree.baseAddress;
|
||||||
|
|
||||||
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
|
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
|
||||||
return makeTextResult("Cannot read at offset " + QString::number(offset), true);
|
return makeTextResult("Cannot read at offset " + QString::number(offset), true);
|
||||||
@@ -907,8 +908,8 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
|
|||||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||||
QString hexStr = args.value("hexBytes").toString().remove(' ');
|
QString hexStr = args.value("hexBytes").toString().remove(' ');
|
||||||
|
|
||||||
if (args.value("baseRelative").toBool())
|
if (!args.value("baseRelative").toBool())
|
||||||
offset -= (int64_t)doc->tree.baseAddress;
|
offset += (int64_t)doc->tree.baseAddress;
|
||||||
|
|
||||||
if (hexStr.size() % 2 != 0)
|
if (hexStr.size() % 2 != 0)
|
||||||
return makeTextResult("Hex string must have even length", true);
|
return makeTextResult("Hex string must have even length", true);
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ public:
|
|||||||
// Examples: "File", "Process", "Socket"
|
// Examples: "File", "Process", "Socket"
|
||||||
virtual QString kind() const { return QStringLiteral("File"); }
|
virtual QString kind() const { return QStringLiteral("File"); }
|
||||||
|
|
||||||
// Base address for providers that offset reads (e.g. process memory).
|
// Initial base address discovered by the provider (e.g. main module base).
|
||||||
|
// Used by the controller to set tree.baseAddress on first attach.
|
||||||
// For file/buffer providers this is always 0.
|
// For file/buffer providers this is always 0.
|
||||||
virtual uint64_t base() const { return 0; }
|
virtual uint64_t base() const { return 0; }
|
||||||
virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); }
|
|
||||||
|
|
||||||
// Resolve an absolute address to a symbol name.
|
// Resolve an absolute address to a symbol name.
|
||||||
// Returns empty string if no symbol is known.
|
// Returns empty string if no symbol is known.
|
||||||
|
|||||||
@@ -1017,7 +1017,7 @@ private slots:
|
|||||||
void testPrimitiveArrayElements() {
|
void testPrimitiveArrayElements() {
|
||||||
// Expanded primitive array should synthesize element lines dynamically
|
// Expanded primitive array should synthesize element lines dynamically
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0x1000;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node root;
|
Node root;
|
||||||
root.kind = NodeKind::Struct;
|
root.kind = NodeKind::Struct;
|
||||||
@@ -1934,7 +1934,7 @@ private slots:
|
|||||||
void testTextIsNonEmpty() {
|
void testTextIsNonEmpty() {
|
||||||
// Verify composed text is actually generated (not empty)
|
// Verify composed text is actually generated (not empty)
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0x1000;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node root;
|
Node root;
|
||||||
root.kind = NodeKind::Struct;
|
root.kind = NodeKind::Struct;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
using namespace rcx;
|
using namespace rcx;
|
||||||
|
|
||||||
static void buildTree(NodeTree& tree) {
|
static void buildTree(NodeTree& tree) {
|
||||||
tree.baseAddress = 0x1000;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node root;
|
Node root;
|
||||||
root.kind = NodeKind::Struct;
|
root.kind = NodeKind::Struct;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ public:
|
|||||||
}
|
}
|
||||||
int size() const override { return m_data.size(); }
|
int size() const override { return m_data.size(); }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
void setBase(uint64_t b) override { m_base = b; }
|
|
||||||
bool isLive() const override { return true; }
|
bool isLive() const override { return true; }
|
||||||
QString name() const override { return QStringLiteral("test"); }
|
QString name() const override { return QStringLiteral("test"); }
|
||||||
QString kind() const override { return QStringLiteral("Process"); }
|
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.
|
// Small tree: one root struct with a few typed fields at known offsets.
|
||||||
// Keeps tests fast and deterministic (no giant PEB tree).
|
// Keeps tests fast and deterministic (no giant PEB tree).
|
||||||
static void buildSmallTree(NodeTree& tree) {
|
static void buildSmallTree(NodeTree& tree) {
|
||||||
tree.baseAddress = 0x1000;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node root;
|
Node root;
|
||||||
root.kind = NodeKind::Struct;
|
root.kind = NodeKind::Struct;
|
||||||
@@ -405,7 +404,8 @@ private slots:
|
|||||||
|
|
||||||
// ── Test: source switch preserves existing base address ──
|
// ── Test: source switch preserves existing base address ──
|
||||||
void testSourceSwitchPreservesBase() {
|
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);
|
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||||
|
|
||||||
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
|
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
|
||||||
@@ -414,16 +414,14 @@ private slots:
|
|||||||
QCOMPARE(newBase, (uint64_t)0x400000);
|
QCOMPARE(newBase, (uint64_t)0x400000);
|
||||||
|
|
||||||
m_doc->provider = prov;
|
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)
|
if (m_doc->tree.baseAddress == 0)
|
||||||
m_doc->tree.baseAddress = newBase;
|
m_doc->tree.baseAddress = newBase;
|
||||||
else
|
|
||||||
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
|
||||||
|
|
||||||
// baseAddress must stay at the original value
|
// baseAddress must stay at the original value
|
||||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||||
// provider base must be synced to match
|
// provider base is unchanged (no setBase sync) — provider reports its own initial base
|
||||||
QCOMPARE(m_doc->provider->base(), (uint64_t)0x1000);
|
QCOMPARE(m_doc->provider->base(), (uint64_t)0x400000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: source switch on fresh doc uses provider default ──
|
// ── Test: source switch on fresh doc uses provider default ──
|
||||||
@@ -437,12 +435,9 @@ private slots:
|
|||||||
m_doc->provider = prov;
|
m_doc->provider = prov;
|
||||||
if (m_doc->tree.baseAddress == 0)
|
if (m_doc->tree.baseAddress == 0)
|
||||||
m_doc->tree.baseAddress = newBase;
|
m_doc->tree.baseAddress = newBase;
|
||||||
else
|
|
||||||
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
|
||||||
|
|
||||||
// Fresh doc should adopt the provider's default base
|
// Fresh doc should adopt the provider's default base
|
||||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
|
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
|
||||||
QCOMPARE(m_doc->provider->base(), (uint64_t)0x7FFE0000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: toggleCollapse + undo ──
|
// ── Test: toggleCollapse + undo ──
|
||||||
|
|||||||
@@ -133,19 +133,18 @@ private slots:
|
|||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
|
|
||||||
void testVTableDisasm_composedAddress() {
|
void testVTableDisasm_composedAddress() {
|
||||||
// Memory layout (provider-relative, i.e. offset from baseAddress):
|
// Memory layout (absolute addresses, baseAddress = 0):
|
||||||
//
|
//
|
||||||
// [0x0000] Root "Obj" struct
|
// [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)
|
// [0x0100] VTable (expanded via pointer deref)
|
||||||
// +0x00: func ptr 0 => value 0xBASE+0x200 (func0 code)
|
// +0x00: func ptr 0 => value 0x200 (func0 code)
|
||||||
// +0x08: func ptr 1 => value 0xBASE+0x300 (func1 code)
|
// +0x08: func ptr 1 => value 0x300 (func1 code)
|
||||||
//
|
//
|
||||||
// [0x0200] func0 code: push rbp; ret
|
// [0x0200] func0 code: push rbp; ret
|
||||||
// [0x0300] func1 code: xor eax, eax; ret
|
// [0x0300] func1 code: xor eax, eax; ret
|
||||||
//
|
//
|
||||||
const uint64_t kBase = 0x7FF600000000ULL;
|
|
||||||
|
|
||||||
// Build a 4KB buffer
|
// Build a 4KB buffer
|
||||||
QByteArray mem(4096, '\0');
|
QByteArray mem(4096, '\0');
|
||||||
@@ -153,12 +152,12 @@ private slots:
|
|||||||
memcpy(mem.data() + off, &val, 8);
|
memcpy(mem.data() + off, &val, 8);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Root object at offset 0: __vptr points to vtable at kBase + 0x100
|
// Root object at offset 0: __vptr points to vtable at 0x100
|
||||||
w64(0x00, kBase + 0x100);
|
w64(0x00, 0x100);
|
||||||
|
|
||||||
// VTable at offset 0x100: two function pointers
|
// VTable at offset 0x100: two function pointers
|
||||||
w64(0x100, kBase + 0x200); // slot 0 -> func0
|
w64(0x100, 0x200); // slot 0 -> func0
|
||||||
w64(0x108, kBase + 0x300); // slot 1 -> func1
|
w64(0x108, 0x300); // slot 1 -> func1
|
||||||
|
|
||||||
// func0 at offset 0x200: push rbp; ret
|
// func0 at offset 0x200: push rbp; ret
|
||||||
mem[0x200] = '\x55';
|
mem[0x200] = '\x55';
|
||||||
@@ -173,7 +172,7 @@ private slots:
|
|||||||
|
|
||||||
// Build node tree
|
// Build node tree
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = kBase;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
// Root struct "Obj"
|
// Root struct "Obj"
|
||||||
Node root;
|
Node root;
|
||||||
@@ -227,8 +226,8 @@ private slots:
|
|||||||
for (int i = 0; i < result.meta.size(); i++) {
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
const LineMeta& lm = result.meta[i];
|
const LineMeta& lm = result.meta[i];
|
||||||
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
|
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
|
||||||
// Only include the pointer-expanded ones (near vtable at kBase+0x100)
|
// Only include the pointer-expanded ones (near vtable at 0x100)
|
||||||
if (lm.offsetAddr >= kBase + 0x100 && lm.offsetAddr < kBase + 0x200) {
|
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
|
||||||
int nodeIdx = lm.nodeIdx;
|
int nodeIdx = lm.nodeIdx;
|
||||||
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
|
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
|
||||||
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
|
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
|
||||||
@@ -239,29 +238,29 @@ private slots:
|
|||||||
QCOMPARE(funcPtrs.size(), 2);
|
QCOMPARE(funcPtrs.size(), 2);
|
||||||
|
|
||||||
// Verify composed addresses point to the vtable, NOT to the root struct
|
// Verify composed addresses point to the vtable, NOT to the root struct
|
||||||
// func0 should be at kBase + 0x100 (vtable + 0)
|
// func0 should be at 0x100 (vtable + 0)
|
||||||
QCOMPARE(funcPtrs[0].offsetAddr, kBase + 0x100);
|
QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
|
||||||
// func1 should be at kBase + 0x108 (vtable + 8)
|
// func1 should be at 0x108 (vtable + 8)
|
||||||
QCOMPARE(funcPtrs[1].offsetAddr, kBase + 0x108);
|
QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
|
||||||
|
|
||||||
// Now simulate what the hover code should do:
|
// Now simulate what the hover code should do:
|
||||||
// Read the function pointer VALUE from the correct provider address
|
// Read the function pointer VALUE from the correct provider address
|
||||||
for (const auto& fp : funcPtrs) {
|
for (const auto& fp : funcPtrs) {
|
||||||
// Provider-relative address = offsetAddr - baseAddress
|
// Provider reads at absolute address directly
|
||||||
uint64_t provAddr = fp.offsetAddr - kBase;
|
uint64_t provAddr = fp.offsetAddr;
|
||||||
|
|
||||||
// Read the pointer value (the function address)
|
// Read the pointer value (the function address)
|
||||||
uint64_t ptrVal = prov.readU64(provAddr);
|
uint64_t ptrVal = prov.readU64(provAddr);
|
||||||
|
|
||||||
// Verify we got the right pointer values
|
// Verify we got the right pointer values
|
||||||
if (fp.name == "func0") {
|
if (fp.name == "func0") {
|
||||||
QCOMPARE(ptrVal, kBase + 0x200);
|
QCOMPARE(ptrVal, (uint64_t)0x200);
|
||||||
} else {
|
} else {
|
||||||
QCOMPARE(ptrVal, kBase + 0x300);
|
QCOMPARE(ptrVal, (uint64_t)0x300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert pointer value to provider-relative for reading code bytes
|
// Read code bytes at the pointer target (absolute address)
|
||||||
uint64_t codeProvAddr = ptrVal - kBase;
|
uint64_t codeProvAddr = ptrVal;
|
||||||
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
|
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
|
||||||
|
|
||||||
// Disassemble and verify
|
// Disassemble and verify
|
||||||
@@ -275,14 +274,14 @@ private slots:
|
|||||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||||
// Verify address in output matches the real function address
|
// 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]));
|
qPrintable("func0 addr wrong: " + lines[0]));
|
||||||
} else {
|
} else {
|
||||||
// Should decode: xor eax, eax; ret
|
// 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_)));
|
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[0]), QStringLiteral("xor eax, eax"));
|
||||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||||
QVERIFY2(lines[0].startsWith("00007ff600000300"),
|
QVERIFY2(lines[0].contains("300"),
|
||||||
qPrintable("func1 addr wrong: " + lines[0]));
|
qPrintable("func1 addr wrong: " + lines[0]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,26 +291,25 @@ private slots:
|
|||||||
// inside the ROOT struct, not the vtable.
|
// inside the ROOT struct, not the vtable.
|
||||||
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
|
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
|
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
|
||||||
// wrongVal0 = kBase + 0x100 (the vptr itself, NOT a function address)
|
// wrongVal0 = 0x100 (the vptr itself, NOT a function address)
|
||||||
QCOMPARE(wrongVal0, kBase + 0x100);
|
QCOMPARE(wrongVal0, (uint64_t)0x100);
|
||||||
// This is the vtable address, not a function — disassembling it would be wrong
|
// 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");
|
"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");
|
"node.offset=8 reads past vptr, not the second function pointer");
|
||||||
}
|
}
|
||||||
|
|
||||||
void testVTableDisasm_wrongAddressGivesWrongCode() {
|
void testVTableDisasm_wrongAddressGivesWrongCode() {
|
||||||
// Demonstrate that using node.offset instead of composed address
|
// Demonstrate that using node.offset instead of composed address
|
||||||
// gives completely wrong disassembly results
|
// gives completely wrong disassembly results
|
||||||
const uint64_t kBase = 0x10000;
|
|
||||||
QByteArray mem(1024, '\0');
|
QByteArray mem(1024, '\0');
|
||||||
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
|
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
|
||||||
|
|
||||||
// Root at 0: vptr -> 0x80
|
// Root at 0: vptr -> 0x80
|
||||||
w64(0x00, kBase + 0x80);
|
w64(0x00, (uint64_t)0x80);
|
||||||
// VTable at 0x80: one func ptr -> 0x100
|
// VTable at 0x80: one func ptr -> 0x100
|
||||||
w64(0x80, kBase + 0x100);
|
w64(0x80, (uint64_t)0x100);
|
||||||
// Code at 0x100: sub rsp, 0x28; nop; ret
|
// Code at 0x100: sub rsp, 0x28; nop; ret
|
||||||
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
|
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
|
||||||
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
|
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)
|
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
|
||||||
uint64_t wrongPtrVal = prov.readU64(0);
|
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)
|
// RIGHT: read from composed address (vtable + 0)
|
||||||
uint64_t rightPtrVal = prov.readU64(0x80);
|
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
|
// Disassemble the RIGHT target
|
||||||
QByteArray rightCode = prov.readBytes(0x100, 128);
|
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');
|
QStringList rightLines = rightAsm.split('\n');
|
||||||
QVERIFY(rightLines.size() >= 3);
|
QVERIFY(rightLines.size() >= 3);
|
||||||
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
|
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
|
||||||
@@ -337,7 +335,7 @@ private slots:
|
|||||||
|
|
||||||
// Disassemble the WRONG target (vtable data, not code!)
|
// Disassemble the WRONG target (vtable data, not code!)
|
||||||
QByteArray wrongCode = prov.readBytes(0x80, 128);
|
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),
|
// The wrong bytes are the vtable entries (pointer values),
|
||||||
// which decode as garbage instructions, not sub/nop/ret
|
// which decode as garbage instructions, not sub/nop/ret
|
||||||
QVERIFY2(!wrongAsm.contains("sub rsp"),
|
QVERIFY2(!wrongAsm.contains("sub rsp"),
|
||||||
@@ -348,9 +346,9 @@ private slots:
|
|||||||
// Full simulation of the hover flow as implemented in editor.cpp:
|
// Full simulation of the hover flow as implemented in editor.cpp:
|
||||||
//
|
//
|
||||||
// 1. Compose the tree to get LineMeta with correct offsetAddr
|
// 1. Compose the tree to get LineMeta with correct offsetAddr
|
||||||
// 2. For each FuncPtr64 line, read pointer value from snapshot/provider
|
// 2. For each FuncPtr64 line, read pointer value from provider
|
||||||
// using lm.offsetAddr - baseAddress (composed address)
|
// using lm.offsetAddr (absolute address)
|
||||||
// 3. Read code bytes from the REAL provider using ptrVal - baseAddress
|
// 3. Read code bytes from the REAL provider using ptrVal directly
|
||||||
// (the real provider can read any process address; snapshot cannot)
|
// (the real provider can read any process address; snapshot cannot)
|
||||||
// 4. Disassemble the code bytes
|
// 4. Disassemble the code bytes
|
||||||
//
|
//
|
||||||
@@ -358,28 +356,25 @@ private slots:
|
|||||||
// the snapshot), step 3 reads from arbitrary code addresses (needs
|
// the snapshot), step 3 reads from arbitrary code addresses (needs
|
||||||
// the real provider, not snapshot).
|
// the real provider, not snapshot).
|
||||||
|
|
||||||
const uint64_t kBase = 0x7FF600000000ULL;
|
|
||||||
QByteArray mem(8192, '\0');
|
QByteArray mem(8192, '\0');
|
||||||
auto w64 = [&](int off, uint64_t val) {
|
auto w64 = [&](int off, uint64_t val) {
|
||||||
memcpy(mem.data() + off, &val, 8);
|
memcpy(mem.data() + off, &val, 8);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Layout:
|
// Layout:
|
||||||
// [0x000] Root struct: __vptr -> vtable at kBase + 0x100
|
// [0x000] Root struct: __vptr -> vtable at 0x100
|
||||||
// [0x100] VTable: func0 -> kBase + 0x1000, func1 -> kBase + 0x1800
|
// [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
|
||||||
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
|
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
|
||||||
// [0x1800] func1 code: xor eax, eax; ret
|
// [0x1800] func1 code: xor eax, eax; ret
|
||||||
w64(0x000, kBase + 0x100); // __vptr
|
w64(0x000, (uint64_t)0x100); // __vptr
|
||||||
w64(0x100, kBase + 0x1000); // vtable[0]
|
w64(0x100, (uint64_t)0x1000); // vtable[0]
|
||||||
w64(0x108, kBase + 0x1800); // vtable[1]
|
w64(0x108, (uint64_t)0x1800); // vtable[1]
|
||||||
// func0 code
|
// func0 code
|
||||||
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
|
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
|
||||||
// func1 code
|
// func1 code
|
||||||
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
|
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
|
||||||
|
|
||||||
// This provider represents the real process memory.
|
// This provider represents the real process memory.
|
||||||
// In production, this is the ProcessMemoryProvider that reads via
|
|
||||||
// ReadProcessMemory at m_base + addr.
|
|
||||||
BufferProvider realProv(mem);
|
BufferProvider realProv(mem);
|
||||||
|
|
||||||
// Build a snapshot that only contains tree-data pages (like the
|
// Build a snapshot that only contains tree-data pages (like the
|
||||||
@@ -392,7 +387,7 @@ private slots:
|
|||||||
|
|
||||||
// Build node tree
|
// Build node tree
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = kBase;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
|
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
|
||||||
root.parentId = 0; root.offset = 0;
|
root.parentId = 0; root.offset = 0;
|
||||||
@@ -423,11 +418,11 @@ private slots:
|
|||||||
const LineMeta& lm = result.meta[i];
|
const LineMeta& lm = result.meta[i];
|
||||||
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
|
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
|
||||||
continue;
|
continue;
|
||||||
if (lm.offsetAddr < kBase + 0x100 || lm.offsetAddr >= kBase + 0x200)
|
if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
|
||||||
continue; // skip standalone VTable definition entries
|
continue; // skip standalone VTable definition entries
|
||||||
|
|
||||||
// --- Hover step 1: read pointer value from snapshot ---
|
// --- 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)
|
// The snapshot has this data (vtable pages are in it)
|
||||||
QVERIFY2(snapProv.isReadable(provAddr, 8),
|
QVERIFY2(snapProv.isReadable(provAddr, 8),
|
||||||
qPrintable(QString("Snapshot should have vtable page at %1")
|
qPrintable(QString("Snapshot should have vtable page at %1")
|
||||||
@@ -437,7 +432,7 @@ private slots:
|
|||||||
|
|
||||||
// --- Hover step 2: read code from REAL provider ---
|
// --- Hover step 2: read code from REAL provider ---
|
||||||
// The snapshot does NOT have the code pages:
|
// The snapshot does NOT have the code pages:
|
||||||
uint64_t codeAddr = ptrVal - tree.baseAddress;
|
uint64_t codeAddr = ptrVal;
|
||||||
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
|
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
|
||||||
"Snapshot should NOT have function code pages");
|
"Snapshot should NOT have function code pages");
|
||||||
// But the real provider does:
|
// But the real provider does:
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ static BufferProvider makeTestProvider() {
|
|||||||
// Build the full _PEB64 tree (0x7D0 bytes), unions mapped to first member
|
// Build the full _PEB64 tree (0x7D0 bytes), unions mapped to first member
|
||||||
static NodeTree makeTestTree() {
|
static NodeTree makeTestTree() {
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0x000000D87B5E5000ULL;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
// Root struct
|
// Root struct
|
||||||
Node root;
|
Node root;
|
||||||
@@ -342,6 +342,95 @@ static NodeTree makeTestTree() {
|
|||||||
return tree;
|
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 {
|
class TestEditor : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private:
|
private:
|
||||||
@@ -1258,7 +1347,7 @@ private slots:
|
|||||||
|
|
||||||
// Build a small tree: root struct with mixed regular (non-hex) + hex fields
|
// Build a small tree: root struct with mixed regular (non-hex) + hex fields
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0x1000;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node root;
|
Node root;
|
||||||
root.kind = NodeKind::Struct;
|
root.kind = NodeKind::Struct;
|
||||||
@@ -1522,6 +1611,440 @@ private slots:
|
|||||||
"found %1 / %2 total (see menu_hover_full.png, menu_hover_item.png)")
|
"found %1 / %2 total (see menu_hover_full.png, menu_hover_item.png)")
|
||||||
.arg(amberPixels).arg(totalPixels)));
|
.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)
|
QTEST_MAIN(TestEditor)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Q_DECLARE_METATYPE(rcx::TypeEntry)
|
|||||||
using namespace rcx;
|
using namespace rcx;
|
||||||
|
|
||||||
static void buildTwoRootTree(NodeTree& tree) {
|
static void buildTwoRootTree(NodeTree& tree) {
|
||||||
tree.baseAddress = 0x1000;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node a;
|
Node a;
|
||||||
a.kind = NodeKind::Struct;
|
a.kind = NodeKind::Struct;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ using namespace rcx;
|
|||||||
// ── Fixture: small tree with diverse field types ──
|
// ── Fixture: small tree with diverse field types ──
|
||||||
|
|
||||||
static void buildValidationTree(NodeTree& tree) {
|
static void buildValidationTree(NodeTree& tree) {
|
||||||
tree.baseAddress = 0x1000;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
Node root;
|
Node root;
|
||||||
root.kind = NodeKind::Struct;
|
root.kind = NodeKind::Struct;
|
||||||
|
|||||||
@@ -260,17 +260,6 @@ private slots:
|
|||||||
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
|
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 ──
|
// ── Read: MZ header on main thread ──
|
||||||
|
|
||||||
void provider_read_mz_mainThread()
|
void provider_read_mz_mainThread()
|
||||||
|
|||||||
Reference in New Issue
Block a user