From ed8a44917bfda4bf97942e0d582b2cfedc7ba6e3 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Sun, 1 Mar 2026 07:42:40 -0700 Subject: [PATCH] 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 --- CMakeLists.txt | 8 + plugins/ProcessMemory/ProcessMemoryPlugin.cpp | 38 +- plugins/ProcessMemory/ProcessMemoryPlugin.h | 2 + .../RemoteProcessMemoryPlugin.cpp | 14 +- .../RemoteProcessMemoryPlugin.h | 2 + .../RemoteProcessMemory/rcx_rpc_protocol.h | 6 +- plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp | 13 + plugins/WinDbgMemory/WinDbgMemoryPlugin.h | 2 + src/addressparser.cpp | 2 + src/compose.cpp | 10 +- src/controller.cpp | 47 +- src/controller.h | 1 + src/core.h | 4 + src/generator.cpp | 19 +- src/imports/import_reclass_xml.cpp | 17 +- src/imports/import_reclass_xml.h | 4 +- src/imports/import_source.cpp | 74 +-- src/imports/import_source.h | 4 +- src/iplugin.h | 3 +- src/main.cpp | 1 + src/processpicker.cpp | 31 +- src/processpicker.h | 1 + src/providers/provider.h | 4 + src/scanner.cpp | 56 ++- src/scanner.h | 8 +- src/scannerpanel.cpp | 47 +- src/scannerpanel.h | 1 + tests/test_32bit_support.cpp | 440 ++++++++++++++++++ 28 files changed, 761 insertions(+), 98 deletions(-) create mode 100644 tests/test_32bit_support.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a2bb5b..1cec270 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp index 522b525..b185f41 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp @@ -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 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 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 diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h index 7dc5f5d..1bf56ef 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.h +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -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 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; diff --git a/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp index 22f78f8..ed69ead 100644 --- a/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp +++ b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp @@ -59,6 +59,10 @@ struct IpcClient { QMutex mutex; bool connected = false; + RcxRpcHeader* header() const { + return mappedView ? reinterpret_cast(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; diff --git a/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h index 6edd16c..6a2e71f 100644 --- a/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h +++ b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h @@ -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 m_ipc; QVector m_modules; }; diff --git a/plugins/RemoteProcessMemory/rcx_rpc_protocol.h b/plugins/RemoteProcessMemory/rcx_rpc_protocol.h index c20f6d8..a882cf5 100644 --- a/plugins/RemoteProcessMemory/rcx_rpc_protocol.h +++ b/plugins/RemoteProcessMemory/rcx_rpc_protocol.h @@ -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) ─────────────────── */ diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp index e8200af..b770770 100644 --- a/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp @@ -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 diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.h b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h index 771b73a..82b1a76 100644 --- a/plugins/WinDbgMemory/WinDbgMemoryPlugin.h +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h @@ -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; diff --git a/src/addressparser.cpp b/src/addressparser.cpp index 77d56a7..10cb121 100644 --- a/src/addressparser.cpp +++ b/src/addressparser.cpp @@ -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, diff --git a/src/compose.cpp b/src/compose.cpp index 9fd862f..c42f3ad 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -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; diff --git a/src/controller.cpp b/src/controller.cpp index 0d33672..80bd009 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -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 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; } diff --git a/src/controller.h b/src/controller.h index da47178..c0d543e 100644 --- a/src/controller.h +++ b/src/controller.h @@ -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* docs) { m_projectDocs = docs; } diff --git a/src/core.h b/src/core.h index 271926f..02006be 100644 --- a/src/core.h +++ b/src/core.h @@ -332,6 +332,7 @@ struct NodeTree { QVector nodes; uint64_t baseAddress = 0x00400000; QString baseAddressFormula; // e.g. " + 0x100" + int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit uint64_t m_nextId = 1; mutable QHash 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) { diff --git a/src/generator.cpp b/src/generator.cpp index f44d582..1560527 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -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; diff --git a/src/imports/import_reclass_xml.cpp b/src/imports/import_reclass_xml.cpp index d7bf1de..e26255d 100644 --- a/src/imports/import_reclass_xml.cpp +++ b/src/imports/import_reclass_xml.cpp @@ -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 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 element if (isClassInstanceArrayType(xmlType, version)) { diff --git a/src/imports/import_reclass_xml.h b/src/imports/import_reclass_xml.h index 775e1c5..6b06d11 100644 --- a/src/imports/import_reclass_xml.h +++ b/src/imports/import_reclass_xml.h @@ -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 diff --git a/src/imports/import_source.cpp b/src/imports/import_source.cpp index 680d013..6ba8e13 100644 --- a/src/imports/import_source.cpp +++ b/src/imports/import_source.cpp @@ -14,8 +14,12 @@ struct TypeInfo { int size; // bytes (0 = dynamic/pointer) }; -static QHash buildTypeTable() { +static QHash buildTypeTable(int ptrSize = 8) { QHash 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 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& pendingRefs; bool useCommentOffsets; QSet 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& 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 typeTable = buildTypeTable(); + // Build type table (pointer-size types depend on target architecture) + QHash 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 classIds; QVector 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) { diff --git a/src/imports/import_source.h b/src/imports/import_source.h index 1110c1c..92fcf97 100644 --- a/src/imports/import_source.h +++ b/src/imports/import_source.h @@ -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 diff --git a/src/iplugin.h b/src/iplugin.h index 3dac1aa..49025ef 100644 --- a/src/iplugin.h +++ b/src/iplugin.h @@ -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) {} diff --git a/src/main.cpp b/src/main.cpp index d5448e8..4f1ca7a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2921,6 +2921,7 @@ void MainWindow::createScannerDock() { if (!ctrl) return; ctrl->document()->tree.baseAddress = addr; ctrl->document()->tree.baseAddressFormula.clear(); + ctrl->resetChangeTracking(); ctrl->refresh(); }); } diff --git a/src/processpicker.cpp b/src/processpicker.cpp index 30d84dc..a1e47ed 100644 --- a/src/processpicker.cpp +++ b/src/processpicker.cpp @@ -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& 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 diff --git a/src/processpicker.h b/src/processpicker.h index dc13232..0779d9a 100644 --- a/src/processpicker.h +++ b/src/processpicker.h @@ -14,6 +14,7 @@ struct ProcessInfo { QString name; QString path; QIcon icon; + bool is32Bit = false; }; class ProcessPicker : public QDialog diff --git a/src/providers/provider.h b/src/providers/provider.h index 9004b24..787647b 100644 --- a/src/providers/provider.h +++ b/src/providers/provider.h @@ -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. diff --git a/src/scanner.cpp b/src/scanner.cpp index 7d7bf79..86c74a6 100644 --- a/src/scanner.cpp +++ b/src/scanner.cpp @@ -521,7 +521,9 @@ done: } void ScanEngine::startRescan(std::shared_ptr provider, - QVector results, int readSize) { + QVector 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, }); 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 ScanEngine::runRescan(std::shared_ptr prov, - QVector results, int readSize) { + QVector 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 ScanEngine::runRescan(std::shared_ptr prov, uint64_t totalBytesRead = 0; int i = 0; + // Track which results matched the filter (by original index) + QVector 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 ScanEngine::runRescan(std::shared_ptr 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 ScanEngine::runRescan(std::shared_ptr prov, } } + // Filter out non-matching results + if (hasFilter) { + QVector 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"; diff --git a/src/scanner.h b/src/scanner.h index 6d2592d..cfe6a21 100644 --- a/src/scanner.h +++ b/src/scanner.h @@ -66,7 +66,9 @@ public: void start(std::shared_ptr provider, const ScanRequest& req); void startRescan(std::shared_ptr provider, - QVector results, int readSize); + QVector results, int readSize, + const QByteArray& filterPattern = {}, + const QByteArray& filterMask = {}); void abort(); bool isRunning() const; @@ -79,7 +81,9 @@ signals: private: QVector runScan(std::shared_ptr prov, const ScanRequest& req); QVector runRescan(std::shared_ptr prov, - QVector results, int readSize); + QVector results, int readSize, + const QByteArray& filterPattern, + const QByteArray& filterMask); std::atomic m_abort{false}; QFutureWatcher>* m_watcher = nullptr; diff --git a/src/scannerpanel.cpp b/src/scannerpanel.cpp index 951754c..2dca342 100644 --- a/src/scannerpanel.cpp +++ b/src/scannerpanel.cpp @@ -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 results) { @@ -449,8 +478,12 @@ void ScannerPanel::onRescanFinished(QVector 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); diff --git a/src/scannerpanel.h b/src/scannerpanel.h index a477662..fac0b50 100644 --- a/src/scannerpanel.h +++ b/src/scannerpanel.h @@ -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; diff --git a/tests/test_32bit_support.cpp b/tests/test_32bit_support.cpp new file mode 100644 index 0000000..c5cfa2b --- /dev/null +++ b/tests/test_32bit_support.cpp @@ -0,0 +1,440 @@ +#include +#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"