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)
|
||||
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)
|
||||
|
||||
@@ -56,8 +56,13 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
|
||||
m_writable = false;
|
||||
}
|
||||
|
||||
if (m_handle)
|
||||
if (m_handle) {
|
||||
// Detect 32-bit (WoW64) process
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(m_handle, &isWow64) && isWow64)
|
||||
m_pointerSize = 4;
|
||||
cacheModules();
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
@@ -192,9 +197,20 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
|
||||
m_writable = false;
|
||||
}
|
||||
|
||||
if (m_fd >= 0)
|
||||
if (m_fd >= 0) {
|
||||
// Detect 32-bit ELF process
|
||||
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
|
||||
QByteArray exePathUtf8 = exePath.toUtf8();
|
||||
int exeFd = ::open(exePathUtf8.constData(), O_RDONLY);
|
||||
if (exeFd >= 0) {
|
||||
unsigned char elfClass = 0;
|
||||
// ELF e_ident[EI_CLASS] is at offset 4
|
||||
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
|
||||
m_pointerSize = 4;
|
||||
::close(exeFd);
|
||||
}
|
||||
cacheModules();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
@@ -525,6 +541,7 @@ bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
info.name = pinfo.name;
|
||||
info.path = pinfo.path;
|
||||
info.icon = pinfo.icon;
|
||||
info.is32Bit = pinfo.is32Bit;
|
||||
processes.append(info);
|
||||
}
|
||||
|
||||
@@ -586,6 +603,11 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
}
|
||||
}
|
||||
|
||||
// Detect 32-bit (WoW64) process
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(hProcess, &isWow64) && isWow64)
|
||||
info.is32Bit = true;
|
||||
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
|
||||
@@ -632,6 +654,16 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
info.name = procName;
|
||||
info.path = resolvedPath;
|
||||
info.icon = defaultIcon;
|
||||
|
||||
// Detect 32-bit ELF process
|
||||
int exeFd = ::open(exePath.toUtf8().constData(), O_RDONLY);
|
||||
if (exeFd >= 0) {
|
||||
unsigned char elfClass = 0;
|
||||
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
|
||||
info.is32Bit = true;
|
||||
::close(exeFd);
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
bool isReadable(uint64_t, int len) const override {
|
||||
#ifdef _WIN32
|
||||
@@ -54,6 +55,7 @@ private:
|
||||
QString m_processName;
|
||||
bool m_writable;
|
||||
uint64_t m_base;
|
||||
int m_pointerSize = 8;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
|
||||
@@ -59,6 +59,10 @@ struct IpcClient {
|
||||
QMutex mutex;
|
||||
bool connected = false;
|
||||
|
||||
RcxRpcHeader* header() const {
|
||||
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
|
||||
}
|
||||
|
||||
~IpcClient() { disconnect(); }
|
||||
|
||||
/* ── connect / disconnect ──────────────────────────────────────── */
|
||||
@@ -285,8 +289,16 @@ RemoteProcessProvider::RemoteProcessProvider(
|
||||
, m_base(0)
|
||||
, m_ipc(std::move(ipc))
|
||||
{
|
||||
if (m_connected)
|
||||
if (m_connected) {
|
||||
cacheModules();
|
||||
// Read pointer size from payload's SHM header (0 means not set → default 8)
|
||||
auto* hdr = m_ipc ? m_ipc->header() : nullptr;
|
||||
if (hdr) {
|
||||
uint32_t ps = hdr->pointerSize;
|
||||
if (ps == 4 || ps == 8)
|
||||
m_pointerSize = (int)ps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteProcessProvider::~RemoteProcessProvider() = default;
|
||||
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
QString kind() const override { return QStringLiteral("RemoteProcess"); }
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
uint64_t symbolToAddress(const QString& n) const override;
|
||||
@@ -45,6 +46,7 @@ private:
|
||||
QString m_processName;
|
||||
bool m_connected;
|
||||
uint64_t m_base;
|
||||
int m_pointerSize = 8;
|
||||
mutable std::shared_ptr<IpcClient> m_ipc;
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
|
||||
@@ -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) ─────────────────── */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -365,13 +365,14 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t {
|
||||
int ptrSz = m_doc->tree.pointerSize;
|
||||
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = prov->read(addr, &val, 8);
|
||||
*ok = prov->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
}
|
||||
auto result = AddressParser::evaluate(s, 8, &cbs);
|
||||
auto result = AddressParser::evaluate(s, m_doc->tree.pointerSize, &cbs);
|
||||
if (result.ok && result.value != m_doc->tree.baseAddress) {
|
||||
uint64_t oldBase = m_doc->tree.baseAddress;
|
||||
QString oldFormula = m_doc->tree.baseAddressFormula;
|
||||
@@ -524,6 +525,14 @@ void RcxController::setTrackValues(bool on) {
|
||||
}
|
||||
}
|
||||
|
||||
void RcxController::resetChangeTracking() {
|
||||
m_changedOffsets.clear();
|
||||
m_valueHistory.clear();
|
||||
m_prevPages.clear();
|
||||
for (auto& lm : m_lastResult.meta)
|
||||
lm.heatLevel = 0;
|
||||
}
|
||||
|
||||
void RcxController::refresh() {
|
||||
// Bracket compose with thread-local doc pointer for type name resolution
|
||||
s_composeDoc = m_doc;
|
||||
@@ -1312,12 +1321,10 @@ void RcxController::convertToTypedPointer(uint64_t nodeId) {
|
||||
if (ni < 0) return;
|
||||
const Node& node = m_doc->tree.nodes[ni];
|
||||
|
||||
// Determine pointer kind from current node size
|
||||
NodeKind ptrKind;
|
||||
if (node.byteSize() >= 8 || node.kind == NodeKind::Pointer64)
|
||||
ptrKind = NodeKind::Pointer64;
|
||||
else
|
||||
ptrKind = NodeKind::Pointer32;
|
||||
// Determine pointer kind from document's target pointer size
|
||||
NodeKind ptrKind = (m_doc->tree.pointerSize >= 8)
|
||||
? NodeKind::Pointer64
|
||||
: NodeKind::Pointer32;
|
||||
|
||||
// Generate unique struct name: "NewClass", "NewClass_2", "NewClass_3", ...
|
||||
QString baseName = QStringLiteral("NewClass");
|
||||
@@ -1344,15 +1351,18 @@ void RcxController::convertToTypedPointer(uint64_t nodeId) {
|
||||
rootStruct.offset = 0;
|
||||
rootStruct.id = m_doc->tree.reserveId();
|
||||
|
||||
// Create child Hex64 fields for the new struct
|
||||
// Create child hex fields for the new struct, sized to target arch
|
||||
constexpr int kDefaultFields = 16;
|
||||
bool is32 = (m_doc->tree.pointerSize < 8);
|
||||
NodeKind hexKind = is32 ? NodeKind::Hex32 : NodeKind::Hex64;
|
||||
int stride = is32 ? 4 : 8;
|
||||
QVector<Node> children;
|
||||
for (int i = 0; i < kDefaultFields; i++) {
|
||||
Node c;
|
||||
c.kind = NodeKind::Hex64;
|
||||
c.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
||||
c.kind = hexKind;
|
||||
c.name = QStringLiteral("field_%1").arg(i * stride, 2, 16, QChar('0'));
|
||||
c.parentId = rootStruct.id;
|
||||
c.offset = i * 8;
|
||||
c.offset = i * stride;
|
||||
c.id = m_doc->tree.reserveId();
|
||||
children.append(c);
|
||||
}
|
||||
@@ -2312,6 +2322,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
||||
if (preModId > 0)
|
||||
popup->setModifier(preModId, preArrayCount);
|
||||
popup->setCurrentNodeSize(nodeSize);
|
||||
popup->setPointerSize(m_doc->tree.pointerSize);
|
||||
|
||||
connect(popup, &TypeSelectorPopup::typeSelected,
|
||||
this, [this, mode, nodeIdx](const TypeEntry& entry, const QString& fullText) {
|
||||
@@ -2794,6 +2805,9 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
||||
// Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
|
||||
// User-initiated source switches go through selectSource() which does update it.
|
||||
|
||||
// Adopt the provider's pointer size for this document
|
||||
m_doc->tree.pointerSize = m_doc->provider->pointerSize();
|
||||
|
||||
// Re-evaluate stored formula against the new provider
|
||||
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
||||
AddressParserCallbacks cbs;
|
||||
@@ -2803,12 +2817,13 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t {
|
||||
int ptrSz = m_doc->tree.pointerSize;
|
||||
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = prov->read(addr, &val, 8);
|
||||
*ok = prov->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, 8, &cbs);
|
||||
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
||||
if (result.ok)
|
||||
m_doc->tree.baseAddress = result.value;
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ public:
|
||||
// Value tracking toggle (per-tab, off by default)
|
||||
bool trackValues() const { return m_trackValues; }
|
||||
void setTrackValues(bool on);
|
||||
void resetChangeTracking();
|
||||
|
||||
// Cross-tab type visibility: point at the project's full document list
|
||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||
|
||||
@@ -332,6 +332,7 @@ struct NodeTree {
|
||||
QVector<Node> nodes;
|
||||
uint64_t baseAddress = 0x00400000;
|
||||
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
||||
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
||||
uint64_t m_nextId = 1;
|
||||
mutable QHash<uint64_t, int> m_idCache;
|
||||
|
||||
@@ -468,6 +469,8 @@ struct NodeTree {
|
||||
o["baseAddress"] = QString::number(baseAddress, 16);
|
||||
if (!baseAddressFormula.isEmpty())
|
||||
o["baseAddressFormula"] = baseAddressFormula;
|
||||
if (pointerSize != 8)
|
||||
o["pointerSize"] = pointerSize;
|
||||
o["nextId"] = QString::number(m_nextId);
|
||||
QJsonArray arr;
|
||||
for (const auto& n : nodes) arr.append(n.toJson());
|
||||
@@ -479,6 +482,7 @@ struct NodeTree {
|
||||
NodeTree t;
|
||||
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
||||
t.baseAddressFormula = o["baseAddressFormula"].toString();
|
||||
t.pointerSize = o["pointerSize"].toInt(8);
|
||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||
QJsonArray arr = o["nodes"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -80,15 +80,19 @@ static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
|
||||
{ 30, NodeKind::Array }, // ClassPointerArray
|
||||
};
|
||||
|
||||
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
|
||||
static NodeKind lookupKind(int xmlType, XmlVersion ver, int ptrSize = 8) {
|
||||
NodeKind k = NodeKind::Hex8;
|
||||
if (ver == XmlVersion::V2016) {
|
||||
for (const auto& e : kTypeMap2016)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
if (e.xmlType == xmlType) { k = e.kind; break; }
|
||||
} else {
|
||||
for (const auto& e : kTypeMap2013)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
if (e.xmlType == xmlType) { k = e.kind; break; }
|
||||
}
|
||||
return NodeKind::Hex8; // fallback
|
||||
// Remap pointer types for 32-bit targets
|
||||
if (ptrSize < 8 && k == NodeKind::Pointer64)
|
||||
k = NodeKind::Pointer32;
|
||||
return k;
|
||||
}
|
||||
|
||||
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
|
||||
@@ -135,7 +139,7 @@ struct PendingRef {
|
||||
QString className;
|
||||
};
|
||||
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointerSize) {
|
||||
qDebug() << "[ImportXML] Opening file:" << filePath;
|
||||
|
||||
QFile file(filePath);
|
||||
@@ -152,6 +156,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
tree.pointerSize = pointerSize;
|
||||
|
||||
// Class name → struct node ID (for pointer resolution)
|
||||
QHash<QString, uint64_t> classIds;
|
||||
@@ -249,7 +254,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeKind kind = lookupKind(xmlType, version);
|
||||
NodeKind kind = lookupKind(xmlType, version, pointerSize);
|
||||
|
||||
// Handle ClassInstanceArray: read child <Array> element
|
||||
if (isClassInstanceArrayType(xmlType, version)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,8 +14,12 @@ struct TypeInfo {
|
||||
int size; // bytes (0 = dynamic/pointer)
|
||||
};
|
||||
|
||||
static QHash<QString, TypeInfo> buildTypeTable() {
|
||||
static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
|
||||
QHash<QString, TypeInfo> t;
|
||||
// Pointer/size_t kinds depend on target architecture
|
||||
NodeKind ptrKind = (ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
NodeKind uintpKind = (ptrSize >= 8) ? NodeKind::UInt64 : NodeKind::UInt32;
|
||||
NodeKind intpKind = (ptrSize >= 8) ? NodeKind::Int64 : NodeKind::Int32;
|
||||
|
||||
// stdint.h
|
||||
t[QStringLiteral("uint8_t")] = {NodeKind::UInt8, 1};
|
||||
@@ -85,35 +89,35 @@ static QHash<QString, TypeInfo> buildTypeTable() {
|
||||
t[QStringLiteral("LONG64")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("INT64")] = {NodeKind::Int64, 8};
|
||||
|
||||
// Platform pointer-size types
|
||||
t[QStringLiteral("PVOID")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPVOID")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HANDLE")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HMODULE")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HWND")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HINSTANCE")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("SIZE_T")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("ULONG_PTR")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("UINT_PTR")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("DWORD_PTR")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("LONG_PTR")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("INT_PTR")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("SSIZE_T")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("uintptr_t")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("intptr_t")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("size_t")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("ptrdiff_t")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("ssize_t")] = {NodeKind::Int64, 8};
|
||||
// Platform pointer-size types (depend on target architecture)
|
||||
t[QStringLiteral("PVOID")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPVOID")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HANDLE")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HMODULE")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HWND")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HINSTANCE")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("SIZE_T")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("ULONG_PTR")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("UINT_PTR")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("DWORD_PTR")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("LONG_PTR")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("INT_PTR")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("SSIZE_T")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("uintptr_t")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("intptr_t")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("size_t")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("ptrdiff_t")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("ssize_t")] = {intpKind, ptrSize};
|
||||
|
||||
// Pointer type aliases
|
||||
t[QStringLiteral("PCHAR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPCSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PCSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PWSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPWSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPCWSTR")]= {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PCWSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PCHAR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPCSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("PCSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("PWSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPWSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPCWSTR")]= {ptrKind, ptrSize};
|
||||
t[QStringLiteral("PCWSTR")] = {ptrKind, ptrSize};
|
||||
|
||||
return t;
|
||||
}
|
||||
@@ -940,6 +944,7 @@ struct BuildContext {
|
||||
QVector<PendingRef>& pendingRefs;
|
||||
bool useCommentOffsets;
|
||||
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
|
||||
int ptrSize = 8; // target pointer size (4 or 8)
|
||||
};
|
||||
|
||||
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
@@ -1018,7 +1023,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
// Pointer field
|
||||
if (field.isPointer) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.kind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
@@ -1032,7 +1037,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
||||
}
|
||||
|
||||
computedOffset = fieldOffset + 8;
|
||||
computedOffset = fieldOffset + ctx.ptrSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1217,7 +1222,7 @@ static bool hasAnyCommentOffset(const QVector<ParsedField>& fields) {
|
||||
|
||||
// ── NodeTree builder ──
|
||||
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int pointerSize) {
|
||||
if (sourceCode.trimmed().isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Empty source code");
|
||||
return {};
|
||||
@@ -1236,8 +1241,8 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Build type table
|
||||
QHash<QString, TypeInfo> typeTable = buildTypeTable();
|
||||
// Build type table (pointer-size types depend on target architecture)
|
||||
QHash<QString, TypeInfo> typeTable = buildTypeTable(pointerSize);
|
||||
|
||||
// Register typedefs into type table
|
||||
for (auto it = parser.typedefs.begin(); it != parser.typedefs.end(); ++it) {
|
||||
@@ -1248,6 +1253,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
tree.pointerSize = pointerSize;
|
||||
|
||||
QHash<QString, uint64_t> classIds;
|
||||
QVector<PendingRef> pendingRefs;
|
||||
@@ -1265,7 +1271,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
enumNames.insert(ps.name);
|
||||
}
|
||||
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames};
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames, pointerSize};
|
||||
|
||||
// Build nodes for each struct/enum
|
||||
for (const auto& ps : parser.structs) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -2921,6 +2921,7 @@ void MainWindow::createScannerDock() {
|
||||
if (!ctrl) return;
|
||||
ctrl->document()->tree.baseAddress = addr;
|
||||
ctrl->document()->tree.baseAddressFormula.clear();
|
||||
ctrl->resetChangeTracking();
|
||||
ctrl->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,7 +100,10 @@ void ProcessPicker::onProcessSelected()
|
||||
|
||||
int row = item->row();
|
||||
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
|
||||
m_selectedName = ui->processTable->item(row, 1)->text();
|
||||
// Use original name stored in UserRole (without architecture suffix)
|
||||
QVariant origName = ui->processTable->item(row, 1)->data(Qt::UserRole);
|
||||
m_selectedName = origName.isValid() ? origName.toString()
|
||||
: ui->processTable->item(row, 1)->text();
|
||||
|
||||
accept();
|
||||
}
|
||||
@@ -158,11 +161,16 @@ void ProcessPicker::enumerateProcesses()
|
||||
{
|
||||
info.path = "";
|
||||
}
|
||||
// Detect 32-bit (WoW64) process
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(hProcess, &isWow64) && isWow64)
|
||||
info.is32Bit = true;
|
||||
|
||||
CloseHandle(hProcess);
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
|
||||
|
||||
} while (Process32NextW(snapshot, &pe32));
|
||||
}
|
||||
|
||||
@@ -204,6 +212,16 @@ void ProcessPicker::enumerateProcesses()
|
||||
info.name = procName;
|
||||
info.path = resolvedPath;
|
||||
info.icon = defaultIcon;
|
||||
|
||||
// Detect 32-bit ELF process
|
||||
QFile exeFile(exePath);
|
||||
if (exeFile.open(QIODevice::ReadOnly)) {
|
||||
QByteArray header = exeFile.read(5);
|
||||
if (header.size() >= 5 && header[4] == 1) // ELFCLASS32
|
||||
info.is32Bit = true;
|
||||
exeFile.close();
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#else
|
||||
@@ -227,11 +245,16 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
|
||||
pidItem->setData(Qt::EditRole, (int)proc.pid);
|
||||
ui->processTable->setItem(i, 0, pidItem);
|
||||
|
||||
// Name column with icon
|
||||
auto* nameItem = new QTableWidgetItem(proc.name);
|
||||
// Name column with icon and architecture indicator
|
||||
QString displayName = proc.is32Bit
|
||||
? proc.name + QStringLiteral(" (32-bit)")
|
||||
: proc.name;
|
||||
auto* nameItem = new QTableWidgetItem(displayName);
|
||||
if (!proc.icon.isNull()) {
|
||||
nameItem->setIcon(proc.icon);
|
||||
}
|
||||
// Store original name for selectedProcessName()
|
||||
nameItem->setData(Qt::UserRole, proc.name);
|
||||
ui->processTable->setItem(i, 1, nameItem);
|
||||
|
||||
// Path column with tooltip for full path
|
||||
|
||||
@@ -14,6 +14,7 @@ struct ProcessInfo {
|
||||
QString name;
|
||||
QString path;
|
||||
QIcon icon;
|
||||
bool is32Bit = false;
|
||||
};
|
||||
|
||||
class ProcessPicker : public QDialog
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -521,7 +521,9 @@ done:
|
||||
}
|
||||
|
||||
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize) {
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask) {
|
||||
if (isRunning()) return;
|
||||
|
||||
m_abort.store(false);
|
||||
@@ -538,20 +540,26 @@ void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
||||
});
|
||||
|
||||
watcher->setFuture(QtConcurrent::run(
|
||||
[this, provider, results = std::move(results), readSize]() mutable {
|
||||
return runRescan(provider, std::move(results), readSize);
|
||||
[this, provider, results = std::move(results), readSize,
|
||||
filterPattern, filterMask]() mutable {
|
||||
return runRescan(provider, std::move(results), readSize,
|
||||
filterPattern, filterMask);
|
||||
}));
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize) {
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask) {
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
int total = results.size();
|
||||
if (total == 0 || !prov) return results;
|
||||
|
||||
qDebug() << "[rescan] start: " << total << "results, readSize:" << readSize;
|
||||
bool hasFilter = !filterPattern.isEmpty();
|
||||
qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize
|
||||
<< "filter:" << (hasFilter ? "yes" : "no");
|
||||
|
||||
// Save previous values
|
||||
for (auto& r : results)
|
||||
@@ -571,6 +579,9 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
uint64_t totalBytesRead = 0;
|
||||
int i = 0;
|
||||
|
||||
// Track which results matched the filter (by original index)
|
||||
QVector<bool> matched(total, !hasFilter); // if no filter, all match
|
||||
|
||||
while (i < total && !m_abort.load()) {
|
||||
uint64_t spanBase = results[order[i]].address;
|
||||
int spanEnd = i;
|
||||
@@ -588,9 +599,28 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
prov->read(spanBase, chunk.data(), chunkLen);
|
||||
|
||||
for (int j = i; j <= spanEnd; j++) {
|
||||
auto& r = results[order[j]];
|
||||
int idx = order[j];
|
||||
auto& r = results[idx];
|
||||
int off = (int)(r.address - spanBase);
|
||||
r.scanValue = chunk.mid(off, readSize);
|
||||
|
||||
// Apply filter: compare re-read bytes against the new pattern
|
||||
if (hasFilter) {
|
||||
int patLen = filterPattern.size();
|
||||
if (r.scanValue.size() >= patLen) {
|
||||
bool ok = true;
|
||||
const char* data = r.scanValue.constData();
|
||||
const char* pat = filterPattern.constData();
|
||||
const char* msk = filterMask.constData();
|
||||
for (int k = 0; k < patLen; k++) {
|
||||
if ((data[k] & msk[k]) != (pat[k] & msk[k])) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
matched[idx] = ok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chunks++;
|
||||
@@ -606,6 +636,20 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out non-matching results
|
||||
if (hasFilter) {
|
||||
QVector<ScanResult> filtered;
|
||||
filtered.reserve(total);
|
||||
for (int k = 0; k < total; k++) {
|
||||
if (matched[k])
|
||||
filtered.append(std::move(results[k]));
|
||||
}
|
||||
qDebug() << "[rescan] done:" << filtered.size() << "/" << total
|
||||
<< "matched in" << timer.elapsed() << "ms |" << chunks
|
||||
<< "chunks," << (totalBytesRead / 1024) << "KB read";
|
||||
return filtered;
|
||||
}
|
||||
|
||||
qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
|
||||
<< timer.elapsed() << "ms |" << chunks << "chunks,"
|
||||
<< (totalBytesRead / 1024) << "KB read";
|
||||
|
||||
@@ -66,7 +66,9 @@ public:
|
||||
|
||||
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
||||
void startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize);
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern = {},
|
||||
const QByteArray& filterMask = {});
|
||||
void abort();
|
||||
bool isRunning() const;
|
||||
|
||||
@@ -79,7 +81,9 @@ signals:
|
||||
private:
|
||||
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
||||
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize);
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask);
|
||||
|
||||
std::atomic<bool> m_abort{false};
|
||||
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;
|
||||
|
||||
@@ -425,13 +425,42 @@ void ScannerPanel::onUpdateClicked() {
|
||||
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
|
||||
// Build filter from current input field
|
||||
QByteArray filterPattern, filterMask;
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode
|
||||
QString err;
|
||||
if (!m_patternEdit->text().trimmed().isEmpty()) {
|
||||
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Value mode
|
||||
QString err;
|
||||
if (!m_valueEdit->text().trimmed().isEmpty()) {
|
||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return;
|
||||
}
|
||||
m_lastValueType = vt;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last pattern so display uses the new value
|
||||
if (!filterPattern.isEmpty())
|
||||
m_lastPattern = filterPattern;
|
||||
|
||||
m_preRescanCount = m_results.size();
|
||||
m_updateBtn->setEnabled(false);
|
||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
|
||||
m_engine->startRescan(prov, m_results, readSize);
|
||||
m_engine->startRescan(prov, m_results, readSize, filterPattern, filterMask);
|
||||
}
|
||||
|
||||
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
||||
@@ -449,8 +478,12 @@ void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
||||
}
|
||||
|
||||
int n = m_results.size();
|
||||
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
|
||||
.arg(n).arg(n == 1 ? "" : "s"));
|
||||
if (m_preRescanCount > 0 && n < m_preRescanCount)
|
||||
m_statusLabel->setText(QStringLiteral("%1 of %2 results match")
|
||||
.arg(n).arg(m_preRescanCount));
|
||||
else
|
||||
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
|
||||
.arg(n).arg(n == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::onGoToAddress() {
|
||||
@@ -497,13 +530,15 @@ void ScannerPanel::onCellEdited(int row, int col) {
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
cbs.readPointer = [p](uint64_t addr, bool* ok) -> uint64_t {
|
||||
int ptrSz = p->pointerSize();
|
||||
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = p->read(addr, &val, 8);
|
||||
*ok = p->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
}
|
||||
auto result = AddressParser::evaluate(text, 8, &cbs);
|
||||
int evalPtrSize = prov ? prov->pointerSize() : 8;
|
||||
auto result = AddressParser::evaluate(text, evalPtrSize, &cbs);
|
||||
if (result.ok) {
|
||||
m_results[row].address = result.value;
|
||||
emit goToAddress(result.value);
|
||||
|
||||
@@ -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;
|
||||
|
||||
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