feat: 32-bit process support, scanner rescan filtering, suppress flash on navigate

- Add pointerSize() to Provider base; WoW64/ELF detection in ProcessMemory,
  WinDbg, and RemoteProcessMemory plugins
- Wire pointer size through NodeTree, source/XML imports, C++ generator,
  controller, compose, address parser, and RPC protocol header
- Add is32Bit to PluginProcessInfo and ProcessInfo; show (32-bit) in picker
- Scanner rescan now filters results against the current input value
- Go-to-address from scanner resets change tracking to prevent false flashing
This commit is contained in:
IChooseYou
2026-03-01 07:42:40 -07:00
committed by IChooseYou
parent ecfac3decf
commit ed8a44917b
28 changed files with 761 additions and 98 deletions

View File

@@ -244,6 +244,14 @@ if(BUILD_TESTING)
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
add_test(NAME test_scanner COMMAND test_scanner)
add_executable(test_32bit_support tests/test_32bit_support.cpp
src/generator.cpp src/imports/import_source.cpp src/imports/import_reclass_xml.cpp
src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_32bit_support PRIVATE src
${CMAKE_SOURCE_DIR}/plugins/RemoteProcessMemory)
target_link_libraries(test_32bit_support PRIVATE ${QT}::Core ${QT}::Widgets ${QT}::Test)
add_test(NAME test_32bit_support COMMAND test_32bit_support)
if(WIN32)
add_executable(test_import_pdb tests/test_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)

View File

@@ -56,8 +56,13 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
m_writable = false;
}
if (m_handle)
if (m_handle) {
// Detect 32-bit (WoW64) process
BOOL isWow64 = FALSE;
if (IsWow64Process(m_handle, &isWow64) && isWow64)
m_pointerSize = 4;
cacheModules();
}
}
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
@@ -192,9 +197,20 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
m_writable = false;
}
if (m_fd >= 0)
if (m_fd >= 0) {
// Detect 32-bit ELF process
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
QByteArray exePathUtf8 = exePath.toUtf8();
int exeFd = ::open(exePathUtf8.constData(), O_RDONLY);
if (exeFd >= 0) {
unsigned char elfClass = 0;
// ELF e_ident[EI_CLASS] is at offset 4
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
m_pointerSize = 4;
::close(exeFd);
}
cacheModules();
}
}
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
@@ -525,6 +541,7 @@ bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
info.name = pinfo.name;
info.path = pinfo.path;
info.icon = pinfo.icon;
info.is32Bit = pinfo.is32Bit;
processes.append(info);
}
@@ -586,6 +603,11 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
}
}
// Detect 32-bit (WoW64) process
BOOL isWow64 = FALSE;
if (IsWow64Process(hProcess, &isWow64) && isWow64)
info.is32Bit = true;
CloseHandle(hProcess);
}
@@ -632,6 +654,16 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
// Detect 32-bit ELF process
int exeFd = ::open(exePath.toUtf8().constData(), O_RDONLY);
if (exeFd >= 0) {
unsigned char elfClass = 0;
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
info.is32Bit = true;
::close(exeFd);
}
processes.append(info);
}
#endif

View File

@@ -28,6 +28,7 @@ public:
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
QVector<rcx::MemoryRegion> enumerateRegions() const override;
bool isReadable(uint64_t, int len) const override {
#ifdef _WIN32
@@ -54,6 +55,7 @@ private:
QString m_processName;
bool m_writable;
uint64_t m_base;
int m_pointerSize = 8;
struct ModuleInfo {
QString name;

View File

@@ -59,6 +59,10 @@ struct IpcClient {
QMutex mutex;
bool connected = false;
RcxRpcHeader* header() const {
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
}
~IpcClient() { disconnect(); }
/* ── connect / disconnect ──────────────────────────────────────── */
@@ -285,8 +289,16 @@ RemoteProcessProvider::RemoteProcessProvider(
, m_base(0)
, m_ipc(std::move(ipc))
{
if (m_connected)
if (m_connected) {
cacheModules();
// Read pointer size from payload's SHM header (0 means not set → default 8)
auto* hdr = m_ipc ? m_ipc->header() : nullptr;
if (hdr) {
uint32_t ps = hdr->pointerSize;
if (ps == 4 || ps == 8)
m_pointerSize = (int)ps;
}
}
}
RemoteProcessProvider::~RemoteProcessProvider() = default;

View File

@@ -32,6 +32,7 @@ public:
QString kind() const override { return QStringLiteral("RemoteProcess"); }
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& n) const override;
@@ -45,6 +46,7 @@ private:
QString m_processName;
bool m_connected;
uint64_t m_base;
int m_pointerSize = 8;
mutable std::shared_ptr<IpcClient> m_ipc;
QVector<ModuleInfo> m_modules;
};

View File

@@ -66,7 +66,8 @@ struct RcxRpcModuleEntry {
* 32 responseCount (4)
* 36 totalDataUsed (4)
* 40 imageBase (8) -- main module base from PEB / procfs
* 48 _pad[4048]
* 48 pointerSize (4) -- 4 for 32-bit, 8 for 64-bit payload
* 52 _pad[4044]
*/
struct RcxRpcHeader {
uint32_t version;
@@ -79,7 +80,8 @@ struct RcxRpcHeader {
uint32_t responseCount;
uint32_t totalDataUsed;
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
uint8_t _pad[RCX_RPC_HEADER_SIZE - 48];
uint32_t pointerSize; /* 4 for 32-bit, 8 for 64-bit payload */
uint8_t _pad[RCX_RPC_HEADER_SIZE - 52];
};
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */

View File

@@ -201,6 +201,19 @@ void WinDbgMemoryProvider::querySessionInfo()
}
}
// Query effective processor type for pointer size detection
if (m_control) {
ULONG procType = 0;
hr = m_control->GetEffectiveProcessorType(&procType);
if (SUCCEEDED(hr)) {
// IMAGE_FILE_MACHINE_I386 = 0x014C
if (procType == 0x014C)
m_pointerSize = 4;
qDebug() << "[WinDbg] EffectiveProcessorType=" << Qt::hex << procType
<< "pointerSize=" << m_pointerSize;
}
}
// WinDbg provides access to the entire virtual address space.
// Do NOT auto-select a module as base — let the user set their
// own base address. m_base stays 0 so the controller won't

View File

@@ -64,6 +64,7 @@ public:
bool isLive() const override { return m_isLive; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
private:
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
@@ -85,6 +86,7 @@ private:
uint64_t m_base = 0;
bool m_isLive = false;
bool m_writable = false;
int m_pointerSize = 8;
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
mutable int m_readFailCount = 0;

View File

@@ -405,6 +405,8 @@ private:
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
const AddressParserCallbacks* cb)
{
// ptrSize is used by the caller to configure the readPointer callback;
// the parser itself doesn't need it directly.
Q_UNUSED(ptrSize);
// WinDbg displays 64-bit addresses with backtick separators for readability,

View File

@@ -554,10 +554,12 @@ void composeParent(ComposeState& state, const NodeTree& tree,
*ok = false;
return 0;
};
cbs.readPointer = [&prov](uint64_t addr, bool* ok) -> uint64_t {
if (prov.isValid() && prov.isReadable(addr, 8)) {
int ps = tree.pointerSize;
cbs.readPointer = [&prov, ps](uint64_t addr, bool* ok) -> uint64_t {
if (prov.isValid() && prov.isReadable(addr, ps)) {
*ok = true;
return prov.readU64(addr);
return (ps >= 8) ? prov.readU64(addr)
: (uint64_t)prov.readU32(addr);
}
*ok = false;
return 0;
@@ -574,7 +576,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
uint64_t staticAddr = 0;
bool exprOk = false;
if (!sf.offsetExpr.isEmpty()) {
auto result = AddressParser::evaluate(sf.offsetExpr, 8, &cbs);
auto result = AddressParser::evaluate(sf.offsetExpr, tree.pointerSize, &cbs);
exprOk = result.ok;
if (result.ok)
staticAddr = result.value;

View File

@@ -365,13 +365,14 @@ void RcxController::connectEditor(RcxEditor* editor) {
*ok = (base != 0);
return base;
};
cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t {
int ptrSz = m_doc->tree.pointerSize;
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = prov->read(addr, &val, 8);
*ok = prov->read(addr, &val, ptrSz);
return val;
};
}
auto result = AddressParser::evaluate(s, 8, &cbs);
auto result = AddressParser::evaluate(s, m_doc->tree.pointerSize, &cbs);
if (result.ok && result.value != m_doc->tree.baseAddress) {
uint64_t oldBase = m_doc->tree.baseAddress;
QString oldFormula = m_doc->tree.baseAddressFormula;
@@ -524,6 +525,14 @@ void RcxController::setTrackValues(bool on) {
}
}
void RcxController::resetChangeTracking() {
m_changedOffsets.clear();
m_valueHistory.clear();
m_prevPages.clear();
for (auto& lm : m_lastResult.meta)
lm.heatLevel = 0;
}
void RcxController::refresh() {
// Bracket compose with thread-local doc pointer for type name resolution
s_composeDoc = m_doc;
@@ -1312,12 +1321,10 @@ void RcxController::convertToTypedPointer(uint64_t nodeId) {
if (ni < 0) return;
const Node& node = m_doc->tree.nodes[ni];
// Determine pointer kind from current node size
NodeKind ptrKind;
if (node.byteSize() >= 8 || node.kind == NodeKind::Pointer64)
ptrKind = NodeKind::Pointer64;
else
ptrKind = NodeKind::Pointer32;
// Determine pointer kind from document's target pointer size
NodeKind ptrKind = (m_doc->tree.pointerSize >= 8)
? NodeKind::Pointer64
: NodeKind::Pointer32;
// Generate unique struct name: "NewClass", "NewClass_2", "NewClass_3", ...
QString baseName = QStringLiteral("NewClass");
@@ -1344,15 +1351,18 @@ void RcxController::convertToTypedPointer(uint64_t nodeId) {
rootStruct.offset = 0;
rootStruct.id = m_doc->tree.reserveId();
// Create child Hex64 fields for the new struct
// Create child hex fields for the new struct, sized to target arch
constexpr int kDefaultFields = 16;
bool is32 = (m_doc->tree.pointerSize < 8);
NodeKind hexKind = is32 ? NodeKind::Hex32 : NodeKind::Hex64;
int stride = is32 ? 4 : 8;
QVector<Node> children;
for (int i = 0; i < kDefaultFields; i++) {
Node c;
c.kind = NodeKind::Hex64;
c.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
c.kind = hexKind;
c.name = QStringLiteral("field_%1").arg(i * stride, 2, 16, QChar('0'));
c.parentId = rootStruct.id;
c.offset = i * 8;
c.offset = i * stride;
c.id = m_doc->tree.reserveId();
children.append(c);
}
@@ -2312,6 +2322,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
if (preModId > 0)
popup->setModifier(preModId, preArrayCount);
popup->setCurrentNodeSize(nodeSize);
popup->setPointerSize(m_doc->tree.pointerSize);
connect(popup, &TypeSelectorPopup::typeSelected,
this, [this, mode, nodeIdx](const TypeEntry& entry, const QString& fullText) {
@@ -2794,6 +2805,9 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
// Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
// User-initiated source switches go through selectSource() which does update it.
// Adopt the provider's pointer size for this document
m_doc->tree.pointerSize = m_doc->provider->pointerSize();
// Re-evaluate stored formula against the new provider
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
AddressParserCallbacks cbs;
@@ -2803,12 +2817,13 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
*ok = (base != 0);
return base;
};
cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t {
int ptrSz = m_doc->tree.pointerSize;
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = prov->read(addr, &val, 8);
*ok = prov->read(addr, &val, ptrSz);
return val;
};
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, 8, &cbs);
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, ptrSz, &cbs);
if (result.ok)
m_doc->tree.baseAddress = result.value;
}

View File

@@ -141,6 +141,7 @@ public:
// Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; }
void setTrackValues(bool on);
void resetChangeTracking();
// Cross-tab type visibility: point at the project's full document list
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }

View File

@@ -332,6 +332,7 @@ struct NodeTree {
QVector<Node> nodes;
uint64_t baseAddress = 0x00400000;
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
uint64_t m_nextId = 1;
mutable QHash<uint64_t, int> m_idCache;
@@ -468,6 +469,8 @@ struct NodeTree {
o["baseAddress"] = QString::number(baseAddress, 16);
if (!baseAddressFormula.isEmpty())
o["baseAddressFormula"] = baseAddressFormula;
if (pointerSize != 8)
o["pointerSize"] = pointerSize;
o["nextId"] = QString::number(m_nextId);
QJsonArray arr;
for (const auto& n : nodes) arr.append(n.toJson());
@@ -479,6 +482,7 @@ struct NodeTree {
NodeTree t;
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
t.baseAddressFormula = o["baseAddressFormula"].toString();
t.pointerSize = o["pointerSize"].toInt(8);
t.m_nextId = o["nextId"].toString("1").toULongLong();
QJsonArray arr = o["nodes"].toArray();
for (const auto& v : arr) {

View File

@@ -132,16 +132,7 @@ static QString emitField(GenContext& ctx, const Node& node, int depth, int baseO
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
case NodeKind::UTF16:
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
case NodeKind::Pointer32: {
if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
QString target = ctx.structName(tree.nodes[refIdx]);
return ind + QStringLiteral("struct %1* %2;").arg(target, name) + oc;
}
}
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + oc;
}
case NodeKind::Pointer32:
case NodeKind::Pointer64: {
if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
@@ -150,7 +141,13 @@ static QString emitField(GenContext& ctx, const Node& node, int depth, int baseO
return ind + QStringLiteral("struct %1* %2;").arg(target, name) + oc;
}
}
return ind + QStringLiteral("void* %1;").arg(name) + oc;
// Native pointer: use void* when this is the target's natural pointer kind
bool isNativePtr = (node.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4)
|| (node.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8);
if (isNativePtr)
return ind + QStringLiteral("void* %1;").arg(name) + oc;
// Cross-size pointer: fall back to raw integer type
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(node.kind), name) + oc;
}
case NodeKind::FuncPtr32:
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;

View File

@@ -80,15 +80,19 @@ static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
{ 30, NodeKind::Array }, // ClassPointerArray
};
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
static NodeKind lookupKind(int xmlType, XmlVersion ver, int ptrSize = 8) {
NodeKind k = NodeKind::Hex8;
if (ver == XmlVersion::V2016) {
for (const auto& e : kTypeMap2016)
if (e.xmlType == xmlType) return e.kind;
if (e.xmlType == xmlType) { k = e.kind; break; }
} else {
for (const auto& e : kTypeMap2013)
if (e.xmlType == xmlType) return e.kind;
if (e.xmlType == xmlType) { k = e.kind; break; }
}
return NodeKind::Hex8; // fallback
// Remap pointer types for 32-bit targets
if (ptrSize < 8 && k == NodeKind::Pointer64)
k = NodeKind::Pointer32;
return k;
}
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
@@ -135,7 +139,7 @@ struct PendingRef {
QString className;
};
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointerSize) {
qDebug() << "[ImportXML] Opening file:" << filePath;
QFile file(filePath);
@@ -152,6 +156,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
NodeTree tree;
tree.baseAddress = 0x00400000;
tree.pointerSize = pointerSize;
// Class name → struct node ID (for pointer resolution)
QHash<QString, uint64_t> classIds;
@@ -249,7 +254,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
continue;
}
NodeKind kind = lookupKind(xmlType, version);
NodeKind kind = lookupKind(xmlType, version, pointerSize);
// Handle ClassInstanceArray: read child <Array> element
if (isClassInstanceArrayType(xmlType, version)) {

View File

@@ -5,7 +5,9 @@ namespace rcx {
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
// pointerSize: 4 for 32-bit targets, 8 for 64-bit (default).
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr,
int pointerSize = 8);
} // namespace rcx

View File

@@ -14,8 +14,12 @@ struct TypeInfo {
int size; // bytes (0 = dynamic/pointer)
};
static QHash<QString, TypeInfo> buildTypeTable() {
static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
QHash<QString, TypeInfo> t;
// Pointer/size_t kinds depend on target architecture
NodeKind ptrKind = (ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
NodeKind uintpKind = (ptrSize >= 8) ? NodeKind::UInt64 : NodeKind::UInt32;
NodeKind intpKind = (ptrSize >= 8) ? NodeKind::Int64 : NodeKind::Int32;
// stdint.h
t[QStringLiteral("uint8_t")] = {NodeKind::UInt8, 1};
@@ -85,35 +89,35 @@ static QHash<QString, TypeInfo> buildTypeTable() {
t[QStringLiteral("LONG64")] = {NodeKind::Int64, 8};
t[QStringLiteral("INT64")] = {NodeKind::Int64, 8};
// Platform pointer-size types
t[QStringLiteral("PVOID")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("LPVOID")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("HANDLE")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("HMODULE")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("HWND")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("HINSTANCE")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("SIZE_T")] = {NodeKind::UInt64, 8};
t[QStringLiteral("ULONG_PTR")] = {NodeKind::UInt64, 8};
t[QStringLiteral("UINT_PTR")] = {NodeKind::UInt64, 8};
t[QStringLiteral("DWORD_PTR")] = {NodeKind::UInt64, 8};
t[QStringLiteral("LONG_PTR")] = {NodeKind::Int64, 8};
t[QStringLiteral("INT_PTR")] = {NodeKind::Int64, 8};
t[QStringLiteral("SSIZE_T")] = {NodeKind::Int64, 8};
t[QStringLiteral("uintptr_t")] = {NodeKind::UInt64, 8};
t[QStringLiteral("intptr_t")] = {NodeKind::Int64, 8};
t[QStringLiteral("size_t")] = {NodeKind::UInt64, 8};
t[QStringLiteral("ptrdiff_t")] = {NodeKind::Int64, 8};
t[QStringLiteral("ssize_t")] = {NodeKind::Int64, 8};
// Platform pointer-size types (depend on target architecture)
t[QStringLiteral("PVOID")] = {ptrKind, ptrSize};
t[QStringLiteral("LPVOID")] = {ptrKind, ptrSize};
t[QStringLiteral("HANDLE")] = {ptrKind, ptrSize};
t[QStringLiteral("HMODULE")] = {ptrKind, ptrSize};
t[QStringLiteral("HWND")] = {ptrKind, ptrSize};
t[QStringLiteral("HINSTANCE")] = {ptrKind, ptrSize};
t[QStringLiteral("SIZE_T")] = {uintpKind, ptrSize};
t[QStringLiteral("ULONG_PTR")] = {uintpKind, ptrSize};
t[QStringLiteral("UINT_PTR")] = {uintpKind, ptrSize};
t[QStringLiteral("DWORD_PTR")] = {uintpKind, ptrSize};
t[QStringLiteral("LONG_PTR")] = {intpKind, ptrSize};
t[QStringLiteral("INT_PTR")] = {intpKind, ptrSize};
t[QStringLiteral("SSIZE_T")] = {intpKind, ptrSize};
t[QStringLiteral("uintptr_t")] = {uintpKind, ptrSize};
t[QStringLiteral("intptr_t")] = {intpKind, ptrSize};
t[QStringLiteral("size_t")] = {uintpKind, ptrSize};
t[QStringLiteral("ptrdiff_t")] = {intpKind, ptrSize};
t[QStringLiteral("ssize_t")] = {intpKind, ptrSize};
// Pointer type aliases
t[QStringLiteral("PCHAR")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("LPSTR")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("LPCSTR")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("PCSTR")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("PWSTR")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("LPWSTR")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("LPCWSTR")]= {NodeKind::Pointer64, 8};
t[QStringLiteral("PCWSTR")] = {NodeKind::Pointer64, 8};
t[QStringLiteral("PCHAR")] = {ptrKind, ptrSize};
t[QStringLiteral("LPSTR")] = {ptrKind, ptrSize};
t[QStringLiteral("LPCSTR")] = {ptrKind, ptrSize};
t[QStringLiteral("PCSTR")] = {ptrKind, ptrSize};
t[QStringLiteral("PWSTR")] = {ptrKind, ptrSize};
t[QStringLiteral("LPWSTR")] = {ptrKind, ptrSize};
t[QStringLiteral("LPCWSTR")]= {ptrKind, ptrSize};
t[QStringLiteral("PCWSTR")] = {ptrKind, ptrSize};
return t;
}
@@ -940,6 +944,7 @@ struct BuildContext {
QVector<PendingRef>& pendingRefs;
bool useCommentOffsets;
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
int ptrSize = 8; // target pointer size (4 or 8)
};
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
@@ -1018,7 +1023,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
// Pointer field
if (field.isPointer) {
Node n;
n.kind = NodeKind::Pointer64;
n.kind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
@@ -1032,7 +1037,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
ctx.pendingRefs.append({nodeId, field.pointerTarget});
}
computedOffset = fieldOffset + 8;
computedOffset = fieldOffset + ctx.ptrSize;
continue;
}
@@ -1217,7 +1222,7 @@ static bool hasAnyCommentOffset(const QVector<ParsedField>& fields) {
// ── NodeTree builder ──
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int pointerSize) {
if (sourceCode.trimmed().isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Empty source code");
return {};
@@ -1236,8 +1241,8 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
return {};
}
// Build type table
QHash<QString, TypeInfo> typeTable = buildTypeTable();
// Build type table (pointer-size types depend on target architecture)
QHash<QString, TypeInfo> typeTable = buildTypeTable(pointerSize);
// Register typedefs into type table
for (auto it = parser.typedefs.begin(); it != parser.typedefs.end(); ++it) {
@@ -1248,6 +1253,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
NodeTree tree;
tree.baseAddress = 0x00400000;
tree.pointerSize = pointerSize;
QHash<QString, uint64_t> classIds;
QVector<PendingRef> pendingRefs;
@@ -1265,7 +1271,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
enumNames.insert(ps.name);
}
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames};
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames, pointerSize};
// Build nodes for each struct/enum
for (const auto& ps : parser.structs) {

View File

@@ -7,7 +7,9 @@ namespace rcx {
// Supports two modes (auto-detected):
// 1. With comment offsets (// 0xNN) - trusts the offset values
// 2. Without comment offsets - computes offsets from type sizes
// pointerSize: 4 for 32-bit targets, 8 for 64-bit (default).
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr,
int pointerSize = 8);
} // namespace rcx

View File

@@ -66,7 +66,8 @@ struct PluginProcessInfo {
QString name;
QString path;
QIcon icon;
bool is32Bit = false;
PluginProcessInfo() : pid(0) {}
PluginProcessInfo(uint32_t p, const QString& n, const QString& pth = QString(), const QIcon& i = QIcon())
: pid(p), name(n), path(pth), icon(i) {}

View File

@@ -2921,6 +2921,7 @@ void MainWindow::createScannerDock() {
if (!ctrl) return;
ctrl->document()->tree.baseAddress = addr;
ctrl->document()->tree.baseAddressFormula.clear();
ctrl->resetChangeTracking();
ctrl->refresh();
});
}

View File

@@ -100,7 +100,10 @@ void ProcessPicker::onProcessSelected()
int row = item->row();
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
m_selectedName = ui->processTable->item(row, 1)->text();
// Use original name stored in UserRole (without architecture suffix)
QVariant origName = ui->processTable->item(row, 1)->data(Qt::UserRole);
m_selectedName = origName.isValid() ? origName.toString()
: ui->processTable->item(row, 1)->text();
accept();
}
@@ -158,11 +161,16 @@ void ProcessPicker::enumerateProcesses()
{
info.path = "";
}
// Detect 32-bit (WoW64) process
BOOL isWow64 = FALSE;
if (IsWow64Process(hProcess, &isWow64) && isWow64)
info.is32Bit = true;
CloseHandle(hProcess);
processes.append(info);
}
} while (Process32NextW(snapshot, &pe32));
}
@@ -204,6 +212,16 @@ void ProcessPicker::enumerateProcesses()
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
// Detect 32-bit ELF process
QFile exeFile(exePath);
if (exeFile.open(QIODevice::ReadOnly)) {
QByteArray header = exeFile.read(5);
if (header.size() >= 5 && header[4] == 1) // ELFCLASS32
info.is32Bit = true;
exeFile.close();
}
processes.append(info);
}
#else
@@ -227,11 +245,16 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
pidItem->setData(Qt::EditRole, (int)proc.pid);
ui->processTable->setItem(i, 0, pidItem);
// Name column with icon
auto* nameItem = new QTableWidgetItem(proc.name);
// Name column with icon and architecture indicator
QString displayName = proc.is32Bit
? proc.name + QStringLiteral(" (32-bit)")
: proc.name;
auto* nameItem = new QTableWidgetItem(displayName);
if (!proc.icon.isNull()) {
nameItem->setIcon(proc.icon);
}
// Store original name for selectedProcessName()
nameItem->setData(Qt::UserRole, proc.name);
ui->processTable->setItem(i, 1, nameItem);
// Path column with tooltip for full path

View File

@@ -14,6 +14,7 @@ struct ProcessInfo {
QString name;
QString path;
QIcon icon;
bool is32Bit = false;
};
class ProcessPicker : public QDialog

View File

@@ -43,6 +43,10 @@ public:
// Examples: "File", "Process", "Socket"
virtual QString kind() const { return QStringLiteral("File"); }
// Native pointer size of the target (4 for 32-bit, 8 for 64-bit).
// Providers should override this to report the target's architecture.
virtual int pointerSize() const { return 8; }
// 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.

View File

@@ -521,7 +521,9 @@ done:
}
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
QVector<ScanResult> results, int readSize) {
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern,
const QByteArray& filterMask) {
if (isRunning()) return;
m_abort.store(false);
@@ -538,20 +540,26 @@ void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
});
watcher->setFuture(QtConcurrent::run(
[this, provider, results = std::move(results), readSize]() mutable {
return runRescan(provider, std::move(results), readSize);
[this, provider, results = std::move(results), readSize,
filterPattern, filterMask]() mutable {
return runRescan(provider, std::move(results), readSize,
filterPattern, filterMask);
}));
}
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize) {
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern,
const QByteArray& filterMask) {
QElapsedTimer timer;
timer.start();
int total = results.size();
if (total == 0 || !prov) return results;
qDebug() << "[rescan] start: " << total << "results, readSize:" << readSize;
bool hasFilter = !filterPattern.isEmpty();
qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize
<< "filter:" << (hasFilter ? "yes" : "no");
// Save previous values
for (auto& r : results)
@@ -571,6 +579,9 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
uint64_t totalBytesRead = 0;
int i = 0;
// Track which results matched the filter (by original index)
QVector<bool> matched(total, !hasFilter); // if no filter, all match
while (i < total && !m_abort.load()) {
uint64_t spanBase = results[order[i]].address;
int spanEnd = i;
@@ -588,9 +599,28 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
prov->read(spanBase, chunk.data(), chunkLen);
for (int j = i; j <= spanEnd; j++) {
auto& r = results[order[j]];
int idx = order[j];
auto& r = results[idx];
int off = (int)(r.address - spanBase);
r.scanValue = chunk.mid(off, readSize);
// Apply filter: compare re-read bytes against the new pattern
if (hasFilter) {
int patLen = filterPattern.size();
if (r.scanValue.size() >= patLen) {
bool ok = true;
const char* data = r.scanValue.constData();
const char* pat = filterPattern.constData();
const char* msk = filterMask.constData();
for (int k = 0; k < patLen; k++) {
if ((data[k] & msk[k]) != (pat[k] & msk[k])) {
ok = false;
break;
}
}
matched[idx] = ok;
}
}
}
chunks++;
@@ -606,6 +636,20 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
}
}
// Filter out non-matching results
if (hasFilter) {
QVector<ScanResult> filtered;
filtered.reserve(total);
for (int k = 0; k < total; k++) {
if (matched[k])
filtered.append(std::move(results[k]));
}
qDebug() << "[rescan] done:" << filtered.size() << "/" << total
<< "matched in" << timer.elapsed() << "ms |" << chunks
<< "chunks," << (totalBytesRead / 1024) << "KB read";
return filtered;
}
qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
<< timer.elapsed() << "ms |" << chunks << "chunks,"
<< (totalBytesRead / 1024) << "KB read";

View File

@@ -66,7 +66,9 @@ public:
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
void startRescan(std::shared_ptr<Provider> provider,
QVector<ScanResult> results, int readSize);
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern = {},
const QByteArray& filterMask = {});
void abort();
bool isRunning() const;
@@ -79,7 +81,9 @@ signals:
private:
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize);
QVector<ScanResult> results, int readSize,
const QByteArray& filterPattern,
const QByteArray& filterMask);
std::atomic<bool> m_abort{false};
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;

View File

@@ -425,13 +425,42 @@ void ScannerPanel::onUpdateClicked() {
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
// Build filter from current input field
QByteArray filterPattern, filterMask;
if (m_lastScanMode == 0) {
// Signature mode
QString err;
if (!m_patternEdit->text().trimmed().isEmpty()) {
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
return;
}
}
} else {
// Value mode
QString err;
if (!m_valueEdit->text().trimmed().isEmpty()) {
auto vt = (ValueType)m_typeCombo->currentData().toInt();
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
return;
}
m_lastValueType = vt;
}
}
// Update last pattern so display uses the new value
if (!filterPattern.isEmpty())
m_lastPattern = filterPattern;
m_preRescanCount = m_results.size();
m_updateBtn->setEnabled(false);
m_scanBtn->setText(QStringLiteral("Cancel"));
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
m_progressBar->setValue(0);
m_progressBar->show();
m_engine->startRescan(prov, m_results, readSize);
m_engine->startRescan(prov, m_results, readSize, filterPattern, filterMask);
}
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
@@ -449,8 +478,12 @@ void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
}
int n = m_results.size();
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
.arg(n).arg(n == 1 ? "" : "s"));
if (m_preRescanCount > 0 && n < m_preRescanCount)
m_statusLabel->setText(QStringLiteral("%1 of %2 results match")
.arg(n).arg(m_preRescanCount));
else
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
.arg(n).arg(n == 1 ? "" : "s"));
}
void ScannerPanel::onGoToAddress() {
@@ -497,13 +530,15 @@ void ScannerPanel::onCellEdited(int row, int col) {
*ok = (base != 0);
return base;
};
cbs.readPointer = [p](uint64_t addr, bool* ok) -> uint64_t {
int ptrSz = p->pointerSize();
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = p->read(addr, &val, 8);
*ok = p->read(addr, &val, ptrSz);
return val;
};
}
auto result = AddressParser::evaluate(text, 8, &cbs);
int evalPtrSize = prov ? prov->pointerSize() : 8;
auto result = AddressParser::evaluate(text, evalPtrSize, &cbs);
if (result.ok) {
m_results[row].address = result.value;
emit goToAddress(result.value);

View File

@@ -104,6 +104,7 @@ private:
int m_lastScanMode = 0; // 0=signature, 1=value
ValueType m_lastValueType = ValueType::Int32;
QByteArray m_lastPattern; // serialized search value
int m_preRescanCount = 0; // result count before last rescan
QString formatValue(const QByteArray& bytes) const;
int valueSize() const;

View File

@@ -0,0 +1,440 @@
#include <QtTest/QTest>
#include "core.h"
#include "generator.h"
#include "imports/import_source.h"
#include "imports/import_reclass_xml.h"
#include "providers/provider.h"
#include "addressparser.h"
#include "iplugin.h"
#include "processpicker.h"
// Include RPC protocol for header size test
#include "rcx_rpc_protocol.h"
using namespace rcx;
// ── Test provider that reports a configurable pointer size ──
class TestProvider32 : public Provider {
public:
QByteArray m_data;
int m_ptrSize;
TestProvider32(int ptrSize, int dataSize = 256)
: m_ptrSize(ptrSize), m_data(dataSize, '\0') {}
bool read(uint64_t addr, void* buf, int len) const override {
if ((int)addr + len > m_data.size()) {
memset(buf, 0, len);
return false;
}
memcpy(buf, m_data.constData() + addr, len);
return true;
}
int size() const override { return m_data.size(); }
int pointerSize() const override { return m_ptrSize; }
};
class Test32BitSupport : public QObject {
Q_OBJECT
private slots:
// ── 1. Provider::pointerSize() default is 8 ──
void providerDefaultPointerSize() {
// NullProvider inherits default
NullProvider np;
QCOMPARE(np.pointerSize(), 8);
}
void providerCustomPointerSize() {
TestProvider32 p32(4);
QCOMPARE(p32.pointerSize(), 4);
TestProvider32 p64(8);
QCOMPARE(p64.pointerSize(), 8);
}
// ── 2. NodeTree pointerSize field ──
void nodeTreeDefaultPointerSize() {
NodeTree tree;
QCOMPARE(tree.pointerSize, 8);
}
void nodeTreePointerSizeRoundTrip() {
// 32-bit tree persists to JSON and back
NodeTree tree;
tree.pointerSize = 4;
tree.baseAddress = 0x00400000;
Node root;
root.kind = NodeKind::Struct;
root.name = "Test";
root.structTypeName = "Test";
root.parentId = 0;
tree.addNode(root);
QJsonObject json = tree.toJson();
QCOMPARE(json["pointerSize"].toInt(), 4);
NodeTree restored = NodeTree::fromJson(json);
QCOMPARE(restored.pointerSize, 4);
}
void nodeTreePointerSizeOmittedForDefault() {
// 64-bit (default) should not write pointerSize key
NodeTree tree;
tree.pointerSize = 8;
QJsonObject json = tree.toJson();
QVERIFY(!json.contains("pointerSize"));
}
void nodeTreePointerSizeDefaultOnMissing() {
// Legacy JSON without pointerSize should default to 8
QJsonObject json;
json["baseAddress"] = "400000";
json["nextId"] = "1";
json["nodes"] = QJsonArray();
NodeTree tree = NodeTree::fromJson(json);
QCOMPARE(tree.pointerSize, 8);
}
// ── 3. Source import respects pointer size ──
void sourceImport64bitDefault() {
QString src = R"(
struct Test {
PVOID ptr; // 0x0
SIZE_T sz; // 0x8
};
)";
QString error;
NodeTree tree = importFromSource(src, &error);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
// Default: 64-bit pointers
bool foundPtr64 = false, foundUInt64 = false;
for (const auto& n : tree.nodes) {
if (n.name == "ptr" && n.kind == NodeKind::Pointer64) foundPtr64 = true;
if (n.name == "sz" && n.kind == NodeKind::UInt64) foundUInt64 = true;
}
QVERIFY2(foundPtr64, "PVOID should be Pointer64 in 64-bit mode");
QVERIFY2(foundUInt64, "SIZE_T should be UInt64 in 64-bit mode");
}
void sourceImport32bit() {
QString src = R"(
struct Test {
PVOID ptr; // 0x0
SIZE_T sz; // 0x4
};
)";
QString error;
NodeTree tree = importFromSource(src, &error, 4);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
QCOMPARE(tree.pointerSize, 4);
bool foundPtr32 = false, foundUInt32 = false;
for (const auto& n : tree.nodes) {
if (n.name == "ptr" && n.kind == NodeKind::Pointer32) foundPtr32 = true;
if (n.name == "sz" && n.kind == NodeKind::UInt32) foundUInt32 = true;
}
QVERIFY2(foundPtr32, "PVOID should be Pointer32 in 32-bit mode");
QVERIFY2(foundUInt32, "SIZE_T should be UInt32 in 32-bit mode");
}
void sourceImportPointerField32bit() {
// A generic pointer (void* field) should become Pointer32 in 32-bit mode
QString src = R"(
struct Test {
void* ptr; // 0x0
int value; // 0x4
};
)";
QString error;
NodeTree tree = importFromSource(src, &error, 4);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
bool foundPtr32 = false;
for (const auto& n : tree.nodes) {
if (n.name == "ptr" && n.kind == NodeKind::Pointer32) foundPtr32 = true;
}
QVERIFY2(foundPtr32, "void* should be Pointer32 in 32-bit mode");
}
void sourceImportPointerSizeTypes32bit() {
// All pointer-size-dependent types should be 32-bit
QString src = R"(
struct Test {
HANDLE h; // 0x0
ULONG_PTR up; // 0x4
LONG_PTR lp; // 0x8
uintptr_t uip; // 0xC
intptr_t ip; // 0x10
size_t sz; // 0x14
LPVOID lv; // 0x18
PCHAR pc; // 0x1C
};
)";
QString error;
NodeTree tree = importFromSource(src, &error, 4);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
for (const auto& n : tree.nodes) {
if (n.parentId == 0) continue; // skip root struct
int sz = n.byteSize();
QVERIFY2(sz == 4,
qPrintable(QString("Field '%1' has size %2, expected 4")
.arg(n.name).arg(sz)));
}
}
// ── 4. Generator respects pointer size ──
void generatorPointer32NativeVoidStar() {
// For 32-bit target, untyped Pointer32 should emit void*
NodeTree tree;
tree.pointerSize = 4;
Node root;
root.kind = NodeKind::Struct;
root.name = "Test";
root.structTypeName = "Test";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node p;
p.kind = NodeKind::Pointer32;
p.name = "ptr";
p.parentId = rootId;
p.offset = 0;
tree.addNode(p);
QString result = renderCpp(tree, rootId);
QVERIFY2(result.contains("void* ptr;"),
qPrintable("32-bit native Pointer32 should emit void*:\n" + result));
}
void generatorPointer64NativeVoidStar() {
// For 64-bit target (default), untyped Pointer64 should emit void*
NodeTree tree;
tree.pointerSize = 8;
Node root;
root.kind = NodeKind::Struct;
root.name = "Test";
root.structTypeName = "Test";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node p;
p.kind = NodeKind::Pointer64;
p.name = "ptr";
p.parentId = rootId;
p.offset = 0;
tree.addNode(p);
QString result = renderCpp(tree, rootId);
QVERIFY2(result.contains("void* ptr;"),
qPrintable("64-bit native Pointer64 should emit void*:\n" + result));
}
void generatorPointer32CrossSizeInt() {
// For 64-bit target, Pointer32 should emit uint32_t (cross-size)
NodeTree tree;
tree.pointerSize = 8;
Node root;
root.kind = NodeKind::Struct;
root.name = "Test";
root.structTypeName = "Test";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node p;
p.kind = NodeKind::Pointer32;
p.name = "ptr32";
p.parentId = rootId;
p.offset = 0;
tree.addNode(p);
QString result = renderCpp(tree, rootId);
QVERIFY2(result.contains("uint32_t ptr32;"),
qPrintable("Cross-size Pointer32 on 64-bit target should emit uint32_t:\n" + result));
}
void generatorTypedPointerBothSizes() {
// Typed pointers (with refId) always emit struct X* regardless of size
NodeTree tree;
tree.pointerSize = 4;
Node target;
target.kind = NodeKind::Struct;
target.name = "Target";
target.structTypeName = "TargetData";
target.parentId = 0;
int ti = tree.addNode(target);
uint64_t targetId = tree.nodes[ti].id;
Node main;
main.kind = NodeKind::Struct;
main.name = "Main";
main.structTypeName = "MainStruct";
main.parentId = 0;
int mi = tree.addNode(main);
uint64_t mainId = tree.nodes[mi].id;
Node p;
p.kind = NodeKind::Pointer32;
p.name = "pTarget";
p.parentId = mainId;
p.offset = 0;
p.refId = targetId;
tree.addNode(p);
QString result = renderCpp(tree, mainId);
QVERIFY2(result.contains("struct TargetData* pTarget;"),
qPrintable("Typed Pointer32 should emit struct X*:\n" + result));
}
// ── 5. RPC protocol header has pointerSize field ──
void rpcHeaderHasPointerSize() {
// Verify the field exists and header is still 4096 bytes
RcxRpcHeader hdr = {};
hdr.pointerSize = 4;
QCOMPARE(hdr.pointerSize, (uint32_t)4);
QCOMPARE((int)sizeof(RcxRpcHeader), RCX_RPC_HEADER_SIZE);
}
// ── 6. PluginProcessInfo has is32Bit field ──
void pluginProcessInfoIs32Bit() {
PluginProcessInfo info;
QCOMPARE(info.is32Bit, false); // default
info.is32Bit = true;
QCOMPARE(info.is32Bit, true);
}
// ── 7. ProcessInfo has is32Bit field ──
void processInfoIs32Bit() {
ProcessInfo info;
QCOMPARE(info.is32Bit, false); // default
info.is32Bit = true;
QCOMPARE(info.is32Bit, true);
}
// ── 8. AddressParser readPointer uses correct size ──
void addressParserReadPointer32bit() {
// Create a test provider with a 32-bit pointer at address 0
TestProvider32 prov(4, 16);
uint32_t val32 = 0xDEADBEEF;
memcpy(prov.m_data.data(), &val32, 4);
// Write garbage in bytes 4-7 to verify we only read 4 bytes
memset(prov.m_data.data() + 4, 0xFF, 4);
AddressParserCallbacks cbs;
int ptrSz = prov.pointerSize();
auto* p = &prov;
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = p->read(addr, &val, ptrSz);
return val;
};
auto result = AddressParser::evaluate("[0]", ptrSz, &cbs);
QVERIFY(result.ok);
QCOMPARE(result.value, (uint64_t)0xDEADBEEF);
}
void addressParserReadPointer64bit() {
TestProvider32 prov(8, 16);
uint64_t val64 = 0x0000DEADBEEF1234ULL;
memcpy(prov.m_data.data(), &val64, 8);
AddressParserCallbacks cbs;
int ptrSz = prov.pointerSize();
auto* p = &prov;
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = p->read(addr, &val, ptrSz);
return val;
};
auto result = AddressParser::evaluate("[0]", ptrSz, &cbs);
QVERIFY(result.ok);
QCOMPARE(result.value, (uint64_t)0x0000DEADBEEF1234ULL);
}
// ── 9. Source import HANDLE/LPVOID remain 64-bit by default ──
void sourceImportBackwardsCompat() {
QString src = R"(
struct Test {
HANDLE h; // 0x0
LPVOID lv; // 0x8
};
)";
QString error;
NodeTree tree = importFromSource(src, &error);
QVERIFY(!tree.nodes.isEmpty());
// Default (no pointerSize arg) should be 64-bit
for (const auto& n : tree.nodes) {
if (n.name == "h") QCOMPARE(n.kind, NodeKind::Pointer64);
if (n.name == "lv") QCOMPARE(n.kind, NodeKind::Pointer64);
}
}
// ── 10. Full round-trip: 32-bit import → generate → verify ──
void fullRoundTrip32bit() {
QString src = R"(
struct EPROCESS_32 {
PVOID Pcb; // 0x0
HANDLE UniqueProcessId; // 0x4
DWORD ActiveProcessLinks; // 0x8
};
)";
QString error;
NodeTree tree = importFromSource(src, &error, 4);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
QCOMPARE(tree.pointerSize, 4);
// Find the root struct
uint64_t rootId = 0;
for (const auto& n : tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
rootId = n.id;
break;
}
}
QVERIFY(rootId != 0);
// Generate C++ code
QString code = renderCpp(tree, rootId);
QVERIFY2(code.contains("void* Pcb;"),
qPrintable("PVOID in 32-bit should generate void*:\n" + code));
QVERIFY2(code.contains("void* UniqueProcessId;"),
qPrintable("HANDLE in 32-bit should generate void*:\n" + code));
// Verify JSON persistence
QJsonObject json = tree.toJson();
QCOMPARE(json["pointerSize"].toInt(), 4);
NodeTree restored = NodeTree::fromJson(json);
QCOMPARE(restored.pointerSize, 4);
}
};
QTEST_MAIN(Test32BitSupport)
#include "test_32bit_support.moc"