mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -244,6 +244,14 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
|
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
|
||||||
add_test(NAME test_scanner COMMAND test_scanner)
|
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)
|
if(WIN32)
|
||||||
add_executable(test_import_pdb tests/test_import_pdb.cpp
|
add_executable(test_import_pdb tests/test_import_pdb.cpp
|
||||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||||
|
|||||||
@@ -56,8 +56,13 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
|
|||||||
m_writable = false;
|
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();
|
cacheModules();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
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;
|
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();
|
cacheModules();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
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.name = pinfo.name;
|
||||||
info.path = pinfo.path;
|
info.path = pinfo.path;
|
||||||
info.icon = pinfo.icon;
|
info.icon = pinfo.icon;
|
||||||
|
info.is32Bit = pinfo.is32Bit;
|
||||||
processes.append(info);
|
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);
|
CloseHandle(hProcess);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,6 +654,16 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
|||||||
info.name = procName;
|
info.name = procName;
|
||||||
info.path = resolvedPath;
|
info.path = resolvedPath;
|
||||||
info.icon = defaultIcon;
|
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);
|
processes.append(info);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public:
|
|||||||
|
|
||||||
bool isLive() const override { return true; }
|
bool isLive() const override { return true; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
|
int pointerSize() const override { return m_pointerSize; }
|
||||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||||
bool isReadable(uint64_t, int len) const override {
|
bool isReadable(uint64_t, int len) const override {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -54,6 +55,7 @@ private:
|
|||||||
QString m_processName;
|
QString m_processName;
|
||||||
bool m_writable;
|
bool m_writable;
|
||||||
uint64_t m_base;
|
uint64_t m_base;
|
||||||
|
int m_pointerSize = 8;
|
||||||
|
|
||||||
struct ModuleInfo {
|
struct ModuleInfo {
|
||||||
QString name;
|
QString name;
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ struct IpcClient {
|
|||||||
QMutex mutex;
|
QMutex mutex;
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
|
|
||||||
|
RcxRpcHeader* header() const {
|
||||||
|
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
~IpcClient() { disconnect(); }
|
~IpcClient() { disconnect(); }
|
||||||
|
|
||||||
/* ── connect / disconnect ──────────────────────────────────────── */
|
/* ── connect / disconnect ──────────────────────────────────────── */
|
||||||
@@ -285,8 +289,16 @@ RemoteProcessProvider::RemoteProcessProvider(
|
|||||||
, m_base(0)
|
, m_base(0)
|
||||||
, m_ipc(std::move(ipc))
|
, m_ipc(std::move(ipc))
|
||||||
{
|
{
|
||||||
if (m_connected)
|
if (m_connected) {
|
||||||
cacheModules();
|
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;
|
RemoteProcessProvider::~RemoteProcessProvider() = default;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public:
|
|||||||
QString kind() const override { return QStringLiteral("RemoteProcess"); }
|
QString kind() const override { return QStringLiteral("RemoteProcess"); }
|
||||||
bool isLive() const override { return true; }
|
bool isLive() const override { return true; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
|
int pointerSize() const override { return m_pointerSize; }
|
||||||
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
|
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
|
||||||
QString getSymbol(uint64_t addr) const override;
|
QString getSymbol(uint64_t addr) const override;
|
||||||
uint64_t symbolToAddress(const QString& n) const override;
|
uint64_t symbolToAddress(const QString& n) const override;
|
||||||
@@ -45,6 +46,7 @@ private:
|
|||||||
QString m_processName;
|
QString m_processName;
|
||||||
bool m_connected;
|
bool m_connected;
|
||||||
uint64_t m_base;
|
uint64_t m_base;
|
||||||
|
int m_pointerSize = 8;
|
||||||
mutable std::shared_ptr<IpcClient> m_ipc;
|
mutable std::shared_ptr<IpcClient> m_ipc;
|
||||||
QVector<ModuleInfo> m_modules;
|
QVector<ModuleInfo> m_modules;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ struct RcxRpcModuleEntry {
|
|||||||
* 32 responseCount (4)
|
* 32 responseCount (4)
|
||||||
* 36 totalDataUsed (4)
|
* 36 totalDataUsed (4)
|
||||||
* 40 imageBase (8) -- main module base from PEB / procfs
|
* 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 {
|
struct RcxRpcHeader {
|
||||||
uint32_t version;
|
uint32_t version;
|
||||||
@@ -79,7 +80,8 @@ struct RcxRpcHeader {
|
|||||||
uint32_t responseCount;
|
uint32_t responseCount;
|
||||||
uint32_t totalDataUsed;
|
uint32_t totalDataUsed;
|
||||||
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
|
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) ─────────────────── */
|
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
|
||||||
|
|||||||
@@ -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.
|
// WinDbg provides access to the entire virtual address space.
|
||||||
// Do NOT auto-select a module as base — let the user set their
|
// 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
|
// own base address. m_base stays 0 so the controller won't
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public:
|
|||||||
|
|
||||||
bool isLive() const override { return m_isLive; }
|
bool isLive() const override { return m_isLive; }
|
||||||
uint64_t base() const override { return m_base; }
|
uint64_t base() const override { return m_base; }
|
||||||
|
int pointerSize() const override { return m_pointerSize; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
||||||
@@ -85,6 +86,7 @@ private:
|
|||||||
uint64_t m_base = 0;
|
uint64_t m_base = 0;
|
||||||
bool m_isLive = false;
|
bool m_isLive = false;
|
||||||
bool m_writable = false;
|
bool m_writable = false;
|
||||||
|
int m_pointerSize = 8;
|
||||||
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
||||||
mutable int m_readFailCount = 0;
|
mutable int m_readFailCount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -405,6 +405,8 @@ private:
|
|||||||
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
|
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
|
||||||
const AddressParserCallbacks* cb)
|
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);
|
Q_UNUSED(ptrSize);
|
||||||
|
|
||||||
// WinDbg displays 64-bit addresses with backtick separators for readability,
|
// WinDbg displays 64-bit addresses with backtick separators for readability,
|
||||||
|
|||||||
@@ -554,10 +554,12 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
*ok = false;
|
*ok = false;
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
cbs.readPointer = [&prov](uint64_t addr, bool* ok) -> uint64_t {
|
int ps = tree.pointerSize;
|
||||||
if (prov.isValid() && prov.isReadable(addr, 8)) {
|
cbs.readPointer = [&prov, ps](uint64_t addr, bool* ok) -> uint64_t {
|
||||||
|
if (prov.isValid() && prov.isReadable(addr, ps)) {
|
||||||
*ok = true;
|
*ok = true;
|
||||||
return prov.readU64(addr);
|
return (ps >= 8) ? prov.readU64(addr)
|
||||||
|
: (uint64_t)prov.readU32(addr);
|
||||||
}
|
}
|
||||||
*ok = false;
|
*ok = false;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -574,7 +576,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
uint64_t staticAddr = 0;
|
uint64_t staticAddr = 0;
|
||||||
bool exprOk = false;
|
bool exprOk = false;
|
||||||
if (!sf.offsetExpr.isEmpty()) {
|
if (!sf.offsetExpr.isEmpty()) {
|
||||||
auto result = AddressParser::evaluate(sf.offsetExpr, 8, &cbs);
|
auto result = AddressParser::evaluate(sf.offsetExpr, tree.pointerSize, &cbs);
|
||||||
exprOk = result.ok;
|
exprOk = result.ok;
|
||||||
if (result.ok)
|
if (result.ok)
|
||||||
staticAddr = result.value;
|
staticAddr = result.value;
|
||||||
|
|||||||
@@ -365,13 +365,14 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
*ok = (base != 0);
|
*ok = (base != 0);
|
||||||
return base;
|
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;
|
uint64_t val = 0;
|
||||||
*ok = prov->read(addr, &val, 8);
|
*ok = prov->read(addr, &val, ptrSz);
|
||||||
return val;
|
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) {
|
if (result.ok && result.value != m_doc->tree.baseAddress) {
|
||||||
uint64_t oldBase = m_doc->tree.baseAddress;
|
uint64_t oldBase = m_doc->tree.baseAddress;
|
||||||
QString oldFormula = m_doc->tree.baseAddressFormula;
|
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() {
|
void RcxController::refresh() {
|
||||||
// Bracket compose with thread-local doc pointer for type name resolution
|
// Bracket compose with thread-local doc pointer for type name resolution
|
||||||
s_composeDoc = m_doc;
|
s_composeDoc = m_doc;
|
||||||
@@ -1312,12 +1321,10 @@ void RcxController::convertToTypedPointer(uint64_t nodeId) {
|
|||||||
if (ni < 0) return;
|
if (ni < 0) return;
|
||||||
const Node& node = m_doc->tree.nodes[ni];
|
const Node& node = m_doc->tree.nodes[ni];
|
||||||
|
|
||||||
// Determine pointer kind from current node size
|
// Determine pointer kind from document's target pointer size
|
||||||
NodeKind ptrKind;
|
NodeKind ptrKind = (m_doc->tree.pointerSize >= 8)
|
||||||
if (node.byteSize() >= 8 || node.kind == NodeKind::Pointer64)
|
? NodeKind::Pointer64
|
||||||
ptrKind = NodeKind::Pointer64;
|
: NodeKind::Pointer32;
|
||||||
else
|
|
||||||
ptrKind = NodeKind::Pointer32;
|
|
||||||
|
|
||||||
// Generate unique struct name: "NewClass", "NewClass_2", "NewClass_3", ...
|
// Generate unique struct name: "NewClass", "NewClass_2", "NewClass_3", ...
|
||||||
QString baseName = QStringLiteral("NewClass");
|
QString baseName = QStringLiteral("NewClass");
|
||||||
@@ -1344,15 +1351,18 @@ void RcxController::convertToTypedPointer(uint64_t nodeId) {
|
|||||||
rootStruct.offset = 0;
|
rootStruct.offset = 0;
|
||||||
rootStruct.id = m_doc->tree.reserveId();
|
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;
|
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;
|
QVector<Node> children;
|
||||||
for (int i = 0; i < kDefaultFields; i++) {
|
for (int i = 0; i < kDefaultFields; i++) {
|
||||||
Node c;
|
Node c;
|
||||||
c.kind = NodeKind::Hex64;
|
c.kind = hexKind;
|
||||||
c.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
c.name = QStringLiteral("field_%1").arg(i * stride, 2, 16, QChar('0'));
|
||||||
c.parentId = rootStruct.id;
|
c.parentId = rootStruct.id;
|
||||||
c.offset = i * 8;
|
c.offset = i * stride;
|
||||||
c.id = m_doc->tree.reserveId();
|
c.id = m_doc->tree.reserveId();
|
||||||
children.append(c);
|
children.append(c);
|
||||||
}
|
}
|
||||||
@@ -2312,6 +2322,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
if (preModId > 0)
|
if (preModId > 0)
|
||||||
popup->setModifier(preModId, preArrayCount);
|
popup->setModifier(preModId, preArrayCount);
|
||||||
popup->setCurrentNodeSize(nodeSize);
|
popup->setCurrentNodeSize(nodeSize);
|
||||||
|
popup->setPointerSize(m_doc->tree.pointerSize);
|
||||||
|
|
||||||
connect(popup, &TypeSelectorPopup::typeSelected,
|
connect(popup, &TypeSelectorPopup::typeSelected,
|
||||||
this, [this, mode, nodeIdx](const TypeEntry& entry, const QString& fullText) {
|
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.
|
// Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
|
||||||
// User-initiated source switches go through selectSource() which does update 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
|
// Re-evaluate stored formula against the new provider
|
||||||
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
||||||
AddressParserCallbacks cbs;
|
AddressParserCallbacks cbs;
|
||||||
@@ -2803,12 +2817,13 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
|||||||
*ok = (base != 0);
|
*ok = (base != 0);
|
||||||
return base;
|
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;
|
uint64_t val = 0;
|
||||||
*ok = prov->read(addr, &val, 8);
|
*ok = prov->read(addr, &val, ptrSz);
|
||||||
return val;
|
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)
|
if (result.ok)
|
||||||
m_doc->tree.baseAddress = result.value;
|
m_doc->tree.baseAddress = result.value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ public:
|
|||||||
// Value tracking toggle (per-tab, off by default)
|
// Value tracking toggle (per-tab, off by default)
|
||||||
bool trackValues() const { return m_trackValues; }
|
bool trackValues() const { return m_trackValues; }
|
||||||
void setTrackValues(bool on);
|
void setTrackValues(bool on);
|
||||||
|
void resetChangeTracking();
|
||||||
|
|
||||||
// Cross-tab type visibility: point at the project's full document list
|
// Cross-tab type visibility: point at the project's full document list
|
||||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ struct NodeTree {
|
|||||||
QVector<Node> nodes;
|
QVector<Node> nodes;
|
||||||
uint64_t baseAddress = 0x00400000;
|
uint64_t baseAddress = 0x00400000;
|
||||||
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
||||||
|
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
||||||
uint64_t m_nextId = 1;
|
uint64_t m_nextId = 1;
|
||||||
mutable QHash<uint64_t, int> m_idCache;
|
mutable QHash<uint64_t, int> m_idCache;
|
||||||
|
|
||||||
@@ -468,6 +469,8 @@ struct NodeTree {
|
|||||||
o["baseAddress"] = QString::number(baseAddress, 16);
|
o["baseAddress"] = QString::number(baseAddress, 16);
|
||||||
if (!baseAddressFormula.isEmpty())
|
if (!baseAddressFormula.isEmpty())
|
||||||
o["baseAddressFormula"] = baseAddressFormula;
|
o["baseAddressFormula"] = baseAddressFormula;
|
||||||
|
if (pointerSize != 8)
|
||||||
|
o["pointerSize"] = pointerSize;
|
||||||
o["nextId"] = QString::number(m_nextId);
|
o["nextId"] = QString::number(m_nextId);
|
||||||
QJsonArray arr;
|
QJsonArray arr;
|
||||||
for (const auto& n : nodes) arr.append(n.toJson());
|
for (const auto& n : nodes) arr.append(n.toJson());
|
||||||
@@ -479,6 +482,7 @@ struct NodeTree {
|
|||||||
NodeTree t;
|
NodeTree t;
|
||||||
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
||||||
t.baseAddressFormula = o["baseAddressFormula"].toString();
|
t.baseAddressFormula = o["baseAddressFormula"].toString();
|
||||||
|
t.pointerSize = o["pointerSize"].toInt(8);
|
||||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||||
QJsonArray arr = o["nodes"].toArray();
|
QJsonArray arr = o["nodes"].toArray();
|
||||||
for (const auto& v : arr) {
|
for (const auto& v : arr) {
|
||||||
|
|||||||
@@ -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;
|
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
||||||
case NodeKind::UTF16:
|
case NodeKind::UTF16:
|
||||||
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
||||||
case NodeKind::Pointer32: {
|
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::Pointer64: {
|
case NodeKind::Pointer64: {
|
||||||
if (node.refId != 0) {
|
if (node.refId != 0) {
|
||||||
int refIdx = tree.indexOfId(node.refId);
|
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("struct %1* %2;").arg(target, 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;
|
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:
|
case NodeKind::FuncPtr32:
|
||||||
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
|
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
|
||||||
|
|||||||
@@ -80,15 +80,19 @@ static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
|
|||||||
{ 30, NodeKind::Array }, // ClassPointerArray
|
{ 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) {
|
if (ver == XmlVersion::V2016) {
|
||||||
for (const auto& e : kTypeMap2016)
|
for (const auto& e : kTypeMap2016)
|
||||||
if (e.xmlType == xmlType) return e.kind;
|
if (e.xmlType == xmlType) { k = e.kind; break; }
|
||||||
} else {
|
} else {
|
||||||
for (const auto& e : kTypeMap2013)
|
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?
|
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
|
||||||
@@ -135,7 +139,7 @@ struct PendingRef {
|
|||||||
QString className;
|
QString className;
|
||||||
};
|
};
|
||||||
|
|
||||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointerSize) {
|
||||||
qDebug() << "[ImportXML] Opening file:" << filePath;
|
qDebug() << "[ImportXML] Opening file:" << filePath;
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
@@ -152,6 +156,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
|||||||
|
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0x00400000;
|
tree.baseAddress = 0x00400000;
|
||||||
|
tree.pointerSize = pointerSize;
|
||||||
|
|
||||||
// Class name → struct node ID (for pointer resolution)
|
// Class name → struct node ID (for pointer resolution)
|
||||||
QHash<QString, uint64_t> classIds;
|
QHash<QString, uint64_t> classIds;
|
||||||
@@ -249,7 +254,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeKind kind = lookupKind(xmlType, version);
|
NodeKind kind = lookupKind(xmlType, version, pointerSize);
|
||||||
|
|
||||||
// Handle ClassInstanceArray: read child <Array> element
|
// Handle ClassInstanceArray: read child <Array> element
|
||||||
if (isClassInstanceArrayType(xmlType, version)) {
|
if (isClassInstanceArrayType(xmlType, version)) {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ namespace rcx {
|
|||||||
|
|
||||||
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
|
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
|
||||||
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
|
// 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.
|
// 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
|
} // namespace rcx
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ struct TypeInfo {
|
|||||||
int size; // bytes (0 = dynamic/pointer)
|
int size; // bytes (0 = dynamic/pointer)
|
||||||
};
|
};
|
||||||
|
|
||||||
static QHash<QString, TypeInfo> buildTypeTable() {
|
static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
|
||||||
QHash<QString, TypeInfo> t;
|
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
|
// stdint.h
|
||||||
t[QStringLiteral("uint8_t")] = {NodeKind::UInt8, 1};
|
t[QStringLiteral("uint8_t")] = {NodeKind::UInt8, 1};
|
||||||
@@ -85,35 +89,35 @@ static QHash<QString, TypeInfo> buildTypeTable() {
|
|||||||
t[QStringLiteral("LONG64")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("LONG64")] = {NodeKind::Int64, 8};
|
||||||
t[QStringLiteral("INT64")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("INT64")] = {NodeKind::Int64, 8};
|
||||||
|
|
||||||
// Platform pointer-size types
|
// Platform pointer-size types (depend on target architecture)
|
||||||
t[QStringLiteral("PVOID")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("PVOID")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("LPVOID")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("LPVOID")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("HANDLE")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("HANDLE")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("HMODULE")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("HMODULE")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("HWND")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("HWND")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("HINSTANCE")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("HINSTANCE")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("SIZE_T")] = {NodeKind::UInt64, 8};
|
t[QStringLiteral("SIZE_T")] = {uintpKind, ptrSize};
|
||||||
t[QStringLiteral("ULONG_PTR")] = {NodeKind::UInt64, 8};
|
t[QStringLiteral("ULONG_PTR")] = {uintpKind, ptrSize};
|
||||||
t[QStringLiteral("UINT_PTR")] = {NodeKind::UInt64, 8};
|
t[QStringLiteral("UINT_PTR")] = {uintpKind, ptrSize};
|
||||||
t[QStringLiteral("DWORD_PTR")] = {NodeKind::UInt64, 8};
|
t[QStringLiteral("DWORD_PTR")] = {uintpKind, ptrSize};
|
||||||
t[QStringLiteral("LONG_PTR")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("LONG_PTR")] = {intpKind, ptrSize};
|
||||||
t[QStringLiteral("INT_PTR")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("INT_PTR")] = {intpKind, ptrSize};
|
||||||
t[QStringLiteral("SSIZE_T")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("SSIZE_T")] = {intpKind, ptrSize};
|
||||||
t[QStringLiteral("uintptr_t")] = {NodeKind::UInt64, 8};
|
t[QStringLiteral("uintptr_t")] = {uintpKind, ptrSize};
|
||||||
t[QStringLiteral("intptr_t")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("intptr_t")] = {intpKind, ptrSize};
|
||||||
t[QStringLiteral("size_t")] = {NodeKind::UInt64, 8};
|
t[QStringLiteral("size_t")] = {uintpKind, ptrSize};
|
||||||
t[QStringLiteral("ptrdiff_t")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("ptrdiff_t")] = {intpKind, ptrSize};
|
||||||
t[QStringLiteral("ssize_t")] = {NodeKind::Int64, 8};
|
t[QStringLiteral("ssize_t")] = {intpKind, ptrSize};
|
||||||
|
|
||||||
// Pointer type aliases
|
// Pointer type aliases
|
||||||
t[QStringLiteral("PCHAR")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("PCHAR")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("LPSTR")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("LPSTR")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("LPCSTR")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("LPCSTR")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("PCSTR")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("PCSTR")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("PWSTR")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("PWSTR")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("LPWSTR")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("LPWSTR")] = {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("LPCWSTR")]= {NodeKind::Pointer64, 8};
|
t[QStringLiteral("LPCWSTR")]= {ptrKind, ptrSize};
|
||||||
t[QStringLiteral("PCWSTR")] = {NodeKind::Pointer64, 8};
|
t[QStringLiteral("PCWSTR")] = {ptrKind, ptrSize};
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
@@ -940,6 +944,7 @@ struct BuildContext {
|
|||||||
QVector<PendingRef>& pendingRefs;
|
QVector<PendingRef>& pendingRefs;
|
||||||
bool useCommentOffsets;
|
bool useCommentOffsets;
|
||||||
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
|
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,
|
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
|
// Pointer field
|
||||||
if (field.isPointer) {
|
if (field.isPointer) {
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Pointer64;
|
n.kind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||||
n.name = field.name;
|
n.name = field.name;
|
||||||
n.parentId = parentId;
|
n.parentId = parentId;
|
||||||
n.offset = fieldOffset;
|
n.offset = fieldOffset;
|
||||||
@@ -1032,7 +1037,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
||||||
}
|
}
|
||||||
|
|
||||||
computedOffset = fieldOffset + 8;
|
computedOffset = fieldOffset + ctx.ptrSize;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1217,7 +1222,7 @@ static bool hasAnyCommentOffset(const QVector<ParsedField>& fields) {
|
|||||||
|
|
||||||
// ── NodeTree builder ──
|
// ── NodeTree builder ──
|
||||||
|
|
||||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int pointerSize) {
|
||||||
if (sourceCode.trimmed().isEmpty()) {
|
if (sourceCode.trimmed().isEmpty()) {
|
||||||
if (errorMsg) *errorMsg = QStringLiteral("Empty source code");
|
if (errorMsg) *errorMsg = QStringLiteral("Empty source code");
|
||||||
return {};
|
return {};
|
||||||
@@ -1236,8 +1241,8 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build type table
|
// Build type table (pointer-size types depend on target architecture)
|
||||||
QHash<QString, TypeInfo> typeTable = buildTypeTable();
|
QHash<QString, TypeInfo> typeTable = buildTypeTable(pointerSize);
|
||||||
|
|
||||||
// Register typedefs into type table
|
// Register typedefs into type table
|
||||||
for (auto it = parser.typedefs.begin(); it != parser.typedefs.end(); ++it) {
|
for (auto it = parser.typedefs.begin(); it != parser.typedefs.end(); ++it) {
|
||||||
@@ -1248,6 +1253,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
|||||||
|
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0x00400000;
|
tree.baseAddress = 0x00400000;
|
||||||
|
tree.pointerSize = pointerSize;
|
||||||
|
|
||||||
QHash<QString, uint64_t> classIds;
|
QHash<QString, uint64_t> classIds;
|
||||||
QVector<PendingRef> pendingRefs;
|
QVector<PendingRef> pendingRefs;
|
||||||
@@ -1265,7 +1271,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
|||||||
enumNames.insert(ps.name);
|
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
|
// Build nodes for each struct/enum
|
||||||
for (const auto& ps : parser.structs) {
|
for (const auto& ps : parser.structs) {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ namespace rcx {
|
|||||||
// Supports two modes (auto-detected):
|
// Supports two modes (auto-detected):
|
||||||
// 1. With comment offsets (// 0xNN) - trusts the offset values
|
// 1. With comment offsets (// 0xNN) - trusts the offset values
|
||||||
// 2. Without comment offsets - computes offsets from type sizes
|
// 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.
|
// 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
|
} // namespace rcx
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ struct PluginProcessInfo {
|
|||||||
QString name;
|
QString name;
|
||||||
QString path;
|
QString path;
|
||||||
QIcon icon;
|
QIcon icon;
|
||||||
|
bool is32Bit = false;
|
||||||
|
|
||||||
PluginProcessInfo() : pid(0) {}
|
PluginProcessInfo() : pid(0) {}
|
||||||
PluginProcessInfo(uint32_t p, const QString& n, const QString& pth = QString(), const QIcon& i = QIcon())
|
PluginProcessInfo(uint32_t p, const QString& n, const QString& pth = QString(), const QIcon& i = QIcon())
|
||||||
|
|||||||
@@ -2921,6 +2921,7 @@ void MainWindow::createScannerDock() {
|
|||||||
if (!ctrl) return;
|
if (!ctrl) return;
|
||||||
ctrl->document()->tree.baseAddress = addr;
|
ctrl->document()->tree.baseAddress = addr;
|
||||||
ctrl->document()->tree.baseAddressFormula.clear();
|
ctrl->document()->tree.baseAddressFormula.clear();
|
||||||
|
ctrl->resetChangeTracking();
|
||||||
ctrl->refresh();
|
ctrl->refresh();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,10 @@ void ProcessPicker::onProcessSelected()
|
|||||||
|
|
||||||
int row = item->row();
|
int row = item->row();
|
||||||
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
|
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();
|
accept();
|
||||||
}
|
}
|
||||||
@@ -158,6 +161,11 @@ void ProcessPicker::enumerateProcesses()
|
|||||||
{
|
{
|
||||||
info.path = "";
|
info.path = "";
|
||||||
}
|
}
|
||||||
|
// Detect 32-bit (WoW64) process
|
||||||
|
BOOL isWow64 = FALSE;
|
||||||
|
if (IsWow64Process(hProcess, &isWow64) && isWow64)
|
||||||
|
info.is32Bit = true;
|
||||||
|
|
||||||
CloseHandle(hProcess);
|
CloseHandle(hProcess);
|
||||||
|
|
||||||
processes.append(info);
|
processes.append(info);
|
||||||
@@ -204,6 +212,16 @@ void ProcessPicker::enumerateProcesses()
|
|||||||
info.name = procName;
|
info.name = procName;
|
||||||
info.path = resolvedPath;
|
info.path = resolvedPath;
|
||||||
info.icon = defaultIcon;
|
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);
|
processes.append(info);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
@@ -227,11 +245,16 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
|
|||||||
pidItem->setData(Qt::EditRole, (int)proc.pid);
|
pidItem->setData(Qt::EditRole, (int)proc.pid);
|
||||||
ui->processTable->setItem(i, 0, pidItem);
|
ui->processTable->setItem(i, 0, pidItem);
|
||||||
|
|
||||||
// Name column with icon
|
// Name column with icon and architecture indicator
|
||||||
auto* nameItem = new QTableWidgetItem(proc.name);
|
QString displayName = proc.is32Bit
|
||||||
|
? proc.name + QStringLiteral(" (32-bit)")
|
||||||
|
: proc.name;
|
||||||
|
auto* nameItem = new QTableWidgetItem(displayName);
|
||||||
if (!proc.icon.isNull()) {
|
if (!proc.icon.isNull()) {
|
||||||
nameItem->setIcon(proc.icon);
|
nameItem->setIcon(proc.icon);
|
||||||
}
|
}
|
||||||
|
// Store original name for selectedProcessName()
|
||||||
|
nameItem->setData(Qt::UserRole, proc.name);
|
||||||
ui->processTable->setItem(i, 1, nameItem);
|
ui->processTable->setItem(i, 1, nameItem);
|
||||||
|
|
||||||
// Path column with tooltip for full path
|
// Path column with tooltip for full path
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct ProcessInfo {
|
|||||||
QString name;
|
QString name;
|
||||||
QString path;
|
QString path;
|
||||||
QIcon icon;
|
QIcon icon;
|
||||||
|
bool is32Bit = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ProcessPicker : public QDialog
|
class ProcessPicker : public QDialog
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ public:
|
|||||||
// Examples: "File", "Process", "Socket"
|
// Examples: "File", "Process", "Socket"
|
||||||
virtual QString kind() const { return QStringLiteral("File"); }
|
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).
|
// Initial base address discovered by the provider (e.g. main module base).
|
||||||
// Used by the controller to set tree.baseAddress on first attach.
|
// Used by the controller to set tree.baseAddress on first attach.
|
||||||
// For file/buffer providers this is always 0.
|
// For file/buffer providers this is always 0.
|
||||||
|
|||||||
@@ -521,7 +521,9 @@ done:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
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;
|
if (isRunning()) return;
|
||||||
|
|
||||||
m_abort.store(false);
|
m_abort.store(false);
|
||||||
@@ -538,20 +540,26 @@ void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher->setFuture(QtConcurrent::run(
|
watcher->setFuture(QtConcurrent::run(
|
||||||
[this, provider, results = std::move(results), readSize]() mutable {
|
[this, provider, results = std::move(results), readSize,
|
||||||
return runRescan(provider, 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> 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;
|
QElapsedTimer timer;
|
||||||
timer.start();
|
timer.start();
|
||||||
|
|
||||||
int total = results.size();
|
int total = results.size();
|
||||||
if (total == 0 || !prov) return results;
|
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
|
// Save previous values
|
||||||
for (auto& r : results)
|
for (auto& r : results)
|
||||||
@@ -571,6 +579,9 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
|||||||
uint64_t totalBytesRead = 0;
|
uint64_t totalBytesRead = 0;
|
||||||
int i = 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()) {
|
while (i < total && !m_abort.load()) {
|
||||||
uint64_t spanBase = results[order[i]].address;
|
uint64_t spanBase = results[order[i]].address;
|
||||||
int spanEnd = i;
|
int spanEnd = i;
|
||||||
@@ -588,9 +599,28 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
|||||||
prov->read(spanBase, chunk.data(), chunkLen);
|
prov->read(spanBase, chunk.data(), chunkLen);
|
||||||
|
|
||||||
for (int j = i; j <= spanEnd; j++) {
|
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);
|
int off = (int)(r.address - spanBase);
|
||||||
r.scanValue = chunk.mid(off, readSize);
|
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++;
|
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"
|
qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
|
||||||
<< timer.elapsed() << "ms |" << chunks << "chunks,"
|
<< timer.elapsed() << "ms |" << chunks << "chunks,"
|
||||||
<< (totalBytesRead / 1024) << "KB read";
|
<< (totalBytesRead / 1024) << "KB read";
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ public:
|
|||||||
|
|
||||||
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
||||||
void startRescan(std::shared_ptr<Provider> provider,
|
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();
|
void abort();
|
||||||
bool isRunning() const;
|
bool isRunning() const;
|
||||||
|
|
||||||
@@ -79,7 +81,9 @@ signals:
|
|||||||
private:
|
private:
|
||||||
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
||||||
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
|
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};
|
std::atomic<bool> m_abort{false};
|
||||||
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;
|
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;
|
||||||
|
|||||||
@@ -425,13 +425,42 @@ void ScannerPanel::onUpdateClicked() {
|
|||||||
|
|
||||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
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_updateBtn->setEnabled(false);
|
||||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||||
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
|
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
|
||||||
m_progressBar->setValue(0);
|
m_progressBar->setValue(0);
|
||||||
m_progressBar->show();
|
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) {
|
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
||||||
@@ -449,6 +478,10 @@ void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int n = m_results.size();
|
int n = m_results.size();
|
||||||
|
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")
|
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
|
||||||
.arg(n).arg(n == 1 ? "" : "s"));
|
.arg(n).arg(n == 1 ? "" : "s"));
|
||||||
}
|
}
|
||||||
@@ -497,13 +530,15 @@ void ScannerPanel::onCellEdited(int row, int col) {
|
|||||||
*ok = (base != 0);
|
*ok = (base != 0);
|
||||||
return base;
|
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;
|
uint64_t val = 0;
|
||||||
*ok = p->read(addr, &val, 8);
|
*ok = p->read(addr, &val, ptrSz);
|
||||||
return val;
|
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) {
|
if (result.ok) {
|
||||||
m_results[row].address = result.value;
|
m_results[row].address = result.value;
|
||||||
emit goToAddress(result.value);
|
emit goToAddress(result.value);
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ private:
|
|||||||
int m_lastScanMode = 0; // 0=signature, 1=value
|
int m_lastScanMode = 0; // 0=signature, 1=value
|
||||||
ValueType m_lastValueType = ValueType::Int32;
|
ValueType m_lastValueType = ValueType::Int32;
|
||||||
QByteArray m_lastPattern; // serialized search value
|
QByteArray m_lastPattern; // serialized search value
|
||||||
|
int m_preRescanCount = 0; // result count before last rescan
|
||||||
|
|
||||||
QString formatValue(const QByteArray& bytes) const;
|
QString formatValue(const QByteArray& bytes) const;
|
||||||
int valueSize() const;
|
int valueSize() const;
|
||||||
|
|||||||
440
tests/test_32bit_support.cpp
Normal file
440
tests/test_32bit_support.cpp
Normal 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"
|
||||||
Reference in New Issue
Block a user