mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add MCP scanner tools, source.modules, reconnect, and constraint regions
Scanner engine: - Add constrainRegions to ScanRequest — callers pass address ranges that are intersected with provider regions before scanning - Merge overlapping/adjacent constraints to prevent duplicate results - Fix final-chunk overlap: skip overlap advance on last chunk to avoid re-scanning the tail of a region MCP tools: - scanner.scan: value scans (int/float types) with optional region constraints, returns first 15 addresses - scanner.scan_pattern: pattern/signature scans with wildcards - source.modules: list loaded modules with base address and size - mcp.reconnect: graceful client disconnect for IDE reconnection - parseInteger() helper for hex string args (avoids JSON double precision loss on 64-bit addresses) - Fix baseRelative semantics in hex.read/hex.write (was inverted) - Auto-set tree.baseAddress from provider after process attach Scanner panel: - runValueScanAndWait() and runPatternScanAndWait() for blocking scan execution from MCP/automation code Tests: 41 new test cases for constrainRegions covering gaps, partial overlap, adjacent regions, writable filter, degenerate ranges, overlapping constraints, boundary patterns, alignment, and value types at region start/end positions.
This commit is contained in:
@@ -1,15 +1,40 @@
|
||||
#include "mcp_bridge.h"
|
||||
#include "addressparser.h"
|
||||
#include "core.h"
|
||||
#include "controller.h"
|
||||
#include "generator.h"
|
||||
#include "mainwindow.h"
|
||||
#include "scanner.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Parse a number from JSON; accepts string (hex "0x..." or decimal) or number.
|
||||
// Use for offset, length, pid, limit, tabIndex, etc. to avoid double precision loss
|
||||
// and to allow clients to send exact values as decimal/hex strings.
|
||||
static int64_t parseInteger(const QJsonValue& v, int64_t defaultVal = 0) {
|
||||
if (v.isUndefined() || v.isNull())
|
||||
return defaultVal;
|
||||
if (v.isString()) {
|
||||
QString s = v.toString().trimmed();
|
||||
if (s.isEmpty())
|
||||
return defaultVal;
|
||||
bool ok;
|
||||
qint64 val = s.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)
|
||||
? s.mid(2).toLongLong(&ok, 16)
|
||||
: s.toLongLong(&ok, 10);
|
||||
return ok ? val : defaultVal;
|
||||
}
|
||||
if (v.isDouble())
|
||||
return static_cast<int64_t>(v.toDouble());
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Construction / lifecycle
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
@@ -337,6 +362,21 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
}}
|
||||
});
|
||||
|
||||
// 3b. source.modules
|
||||
tools.append(QJsonObject{
|
||||
{"name", "source.modules"},
|
||||
{"description", "List modules for the current data source. Returns name, base (hex), and size for each module. "
|
||||
"Only available when the provider reports module info (e.g. after attaching to a process). "
|
||||
"Use these names in baseAddressFormula for tree base, e.g. '<Module.exe> + 0x1000'."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
|
||||
// 4. hex.read
|
||||
tools.append(QJsonObject{
|
||||
{"name", "hex.read"},
|
||||
@@ -459,22 +499,73 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
}}
|
||||
});
|
||||
|
||||
|
||||
// process.info
|
||||
// 10. scanner.scan
|
||||
tools.append(QJsonObject{
|
||||
{"name", "process.info"},
|
||||
{"description", "Returns PEB address and enumerates all Thread Environment Blocks (TEBs) for the attached process. "
|
||||
"TEBs are discovered via NtQuerySystemInformation and NtQueryInformationThread. "
|
||||
"Each TEB entry includes: address, threadId. "
|
||||
"Requires a live process provider with PEB support."},
|
||||
{"name", "scanner.scan"},
|
||||
{"description", "Run a value scan on the active tab's provider and wait for completion. "
|
||||
"Use after source.switch (e.g. attach to process). Value type: int8, int16, int32, int64, "
|
||||
"uint8, uint16, uint32, uint64, float, double. Results appear in the Scanner panel. "
|
||||
"For value scans (e.g. float 120) prefer scanning readable/writable (data) regions, not executable: "
|
||||
"set filterWritable: true and filterExecutable: false. "
|
||||
"Use 'regions' to restrict scan to specific address ranges (intersected with provider regions)."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}}
|
||||
}}
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
||||
{"valueType", QJsonObject{{"type", "string"},
|
||||
{"description", "Value type: float, double, int32, uint32, int64, uint64, int16, uint16, int8, uint8."}}},
|
||||
{"value", QJsonObject{{"type", "string"},
|
||||
{"description", "Value to search for (e.g. \"120\" for float 120)."}}},
|
||||
{"filterExecutable", QJsonObject{{"type", "boolean"},
|
||||
{"description", "Only scan executable regions (default false). For value scans use false; use writable instead."}}},
|
||||
{"filterWritable", QJsonObject{{"type", "boolean"},
|
||||
{"description", "Only scan writable regions (default false). Recommended true for value scans to hit data/heap, not code."}}},
|
||||
{"regions", QJsonObject{{"type", "array"},
|
||||
{"description", "Restrict scan to these address ranges. Each element is [startHex, endHex], e.g. [[\"0x10000\",\"0x20000\"],[\"0x50000\",\"0x60000\"]]. Ranges are intersected with the provider's real memory regions."},
|
||||
{"items", QJsonObject{{"type", "array"}, {"items", QJsonObject{{"type", "string"}}}}}}}
|
||||
}},
|
||||
{"required", QJsonArray{"valueType", "value"}}
|
||||
}}
|
||||
});
|
||||
|
||||
// 10. scanner.scan_pattern
|
||||
tools.append(QJsonObject{
|
||||
{"name", "scanner.scan_pattern"},
|
||||
{"description", "Run a pattern/signature scan on the active tab's provider and wait for completion. "
|
||||
"Pattern is space-separated hex bytes, e.g. '00 00 20 42 00 00 20 42'. Use ?? for wildcards. "
|
||||
"Results appear in the Scanner panel. Uses the same region list as value scans. "
|
||||
"Use 'regions' to restrict scan to specific address ranges (intersected with provider regions)."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
||||
{"pattern", QJsonObject{{"type", "string"},
|
||||
{"description", "Hex pattern, e.g. '00 00 20 42 00 00 20 42 00 00 00 00 00 00 00 00'. Use ?? for wildcard bytes."}}},
|
||||
{"filterExecutable", QJsonObject{{"type", "boolean"},
|
||||
{"description", "Only scan executable regions (default false)."}}},
|
||||
{"filterWritable", QJsonObject{{"type", "boolean"},
|
||||
{"description", "Only scan writable regions (default false)."}}},
|
||||
{"regions", QJsonObject{{"type", "array"},
|
||||
{"description", "Restrict scan to these address ranges. Each element is [startHex, endHex], e.g. [[\"0x10000\",\"0x20000\"],[\"0x50000\",\"0x60000\"]]. Ranges are intersected with the provider's real memory regions."},
|
||||
{"items", QJsonObject{{"type", "array"}, {"items", QJsonObject{{"type", "string"}}}}}}}
|
||||
}},
|
||||
{"required", QJsonArray{"pattern"}}
|
||||
}}
|
||||
});
|
||||
|
||||
// 11. mcp.reconnect
|
||||
tools.append(QJsonObject{
|
||||
{"name", "mcp.reconnect"},
|
||||
{"description", "Disconnect the current MCP client so it can reconnect to Reclass (e.g. after Reclass was restarted or to reset connection state). "
|
||||
"The client process will exit; your IDE may restart it automatically, reconnecting to Reclass like at startup."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{}}
|
||||
}}
|
||||
});
|
||||
|
||||
return okReply(id, QJsonObject{{"tools", tools}});
|
||||
}
|
||||
|
||||
@@ -494,13 +585,16 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
if (toolName == "project.state") result = toolProjectState(args);
|
||||
else if (toolName == "tree.apply") result = toolTreeApply(args);
|
||||
else if (toolName == "source.switch") result = toolSourceSwitch(args);
|
||||
else if (toolName == "source.modules") result = toolSourceModules(args);
|
||||
else if (toolName == "hex.read") result = toolHexRead(args);
|
||||
else if (toolName == "hex.write") result = toolHexWrite(args);
|
||||
else if (toolName == "status.set") result = toolStatusSet(args);
|
||||
else if (toolName == "ui.action") result = toolUiAction(args);
|
||||
else if (toolName == "tree.search") result = toolTreeSearch(args);
|
||||
else if (toolName == "node.history") result = toolNodeHistory(args);
|
||||
else if (toolName == "process.info") result = toolProcessInfo(args);
|
||||
else if (toolName == "scanner.scan") result = toolScannerScan(args);
|
||||
else if (toolName == "scanner.scan_pattern") result = toolScannerScanPattern(args);
|
||||
else if (toolName == "mcp.reconnect") result = toolReconnect(args);
|
||||
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||
|
||||
m_mainWindow->clearMcpStatus();
|
||||
@@ -529,7 +623,7 @@ QString McpBridge::resolvePlaceholder(const QString& ref,
|
||||
MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args) {
|
||||
// 1) Explicit tab index from args
|
||||
if (args.contains("tabIndex")) {
|
||||
int idx = args.value("tabIndex").toInt();
|
||||
int idx = (int)parseInteger(args.value("tabIndex"));
|
||||
auto* t = m_mainWindow->tabByIndex(idx);
|
||||
if (t) return t;
|
||||
}
|
||||
@@ -561,16 +655,18 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
auto* ctrl = tab->ctrl;
|
||||
const auto& tree = doc->tree;
|
||||
|
||||
int maxDepth = args.value("depth").toInt(1);
|
||||
int maxDepth = (int)parseInteger(args.value("depth"), 1);
|
||||
bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true;
|
||||
bool includeMembers = args.value("includeMembers").toBool(false);
|
||||
int limit = qBound(1, args.value("limit").toInt(50), 500);
|
||||
int offset = qMax(0, args.value("offset").toInt(0));
|
||||
int limit = qBound(1, (int)parseInteger(args.value("limit"), 50), 500);
|
||||
int offset = qMax(0, (int)parseInteger(args.value("offset"), 0));
|
||||
QString parentIdStr = args.value("parentId").toString();
|
||||
uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();
|
||||
|
||||
QJsonObject state;
|
||||
state["baseAddress"] = "0x" + QString::number(tree.baseAddress, 16).toUpper();
|
||||
if (!tree.baseAddressFormula.isEmpty())
|
||||
state["baseAddressFormula"] = tree.baseAddressFormula;
|
||||
state["viewRootId"] = QString::number(ctrl->viewRootId());
|
||||
state["nodeCount"] = tree.nodes.size();
|
||||
|
||||
@@ -686,6 +782,8 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
|
||||
QJsonObject treeObj;
|
||||
treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
|
||||
if (!tree.baseAddressFormula.isEmpty())
|
||||
treeObj["baseAddressFormula"] = tree.baseAddressFormula;
|
||||
treeObj["nextId"] = QString::number(tree.m_nextId);
|
||||
treeObj["nodes"] = nodeArr;
|
||||
treeObj["returned"] = emitted;
|
||||
@@ -750,12 +848,12 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
n.name = op.value("name").toString();
|
||||
QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders);
|
||||
n.parentId = pid.toULongLong();
|
||||
n.offset = op.value("offset").toInt(0);
|
||||
n.offset = (int)parseInteger(op.value("offset"), 0);
|
||||
n.structTypeName = op.value("structTypeName").toString();
|
||||
n.classKeyword = op.value("classKeyword").toString();
|
||||
n.strLen = op.value("strLen").toInt(64);
|
||||
n.strLen = (int)parseInteger(op.value("strLen"), 64);
|
||||
n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
|
||||
n.arrayLen = op.value("arrayLen").toInt(1);
|
||||
n.arrayLen = (int)parseInteger(op.value("arrayLen"), 1);
|
||||
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders);
|
||||
n.refId = refStr.toULongLong();
|
||||
|
||||
@@ -822,7 +920,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
|
||||
int idx = tree.indexOfId(nid.toULongLong());
|
||||
if (idx >= 0) {
|
||||
int newOff = op.value("offset").toInt();
|
||||
int newOff = (int)parseInteger(op.value("offset"));
|
||||
doc->undoStack.push(new RcxCommand(ctrl,
|
||||
cmd::ChangeOffset{tree.nodes[idx].id, tree.nodes[idx].offset, newOff}));
|
||||
applied++;
|
||||
@@ -882,7 +980,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
int idx = tree.indexOfId(nid.toULongLong());
|
||||
if (idx >= 0) {
|
||||
NodeKind newElemKind = kindFromString(op.value("elementKind").toString());
|
||||
int newLen = op.value("arrayLen").toInt(1);
|
||||
int newLen = (int)parseInteger(op.value("arrayLen"), 1);
|
||||
doc->undoStack.push(new RcxCommand(ctrl,
|
||||
cmd::ChangeArrayMeta{tree.nodes[idx].id,
|
||||
tree.nodes[idx].elementKind, newElemKind,
|
||||
@@ -951,7 +1049,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
||||
auto* doc = tab->doc;
|
||||
|
||||
if (args.contains("sourceIndex")) {
|
||||
int idx = args.value("sourceIndex").toInt();
|
||||
int idx = (int)parseInteger(args.value("sourceIndex"));
|
||||
const auto& sources = ctrl->savedSources();
|
||||
if (idx < 0 || idx >= sources.size())
|
||||
return makeTextResult("Source index out of range: " + QString::number(idx), true);
|
||||
@@ -968,11 +1066,17 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
||||
}
|
||||
|
||||
if (args.contains("pid")) {
|
||||
uint32_t pid = (uint32_t)args.value("pid").toInt();
|
||||
uint32_t pid = (uint32_t)parseInteger(args.value("pid"));
|
||||
QString name = args.value("processName").toString();
|
||||
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
||||
QString target = QString("%1:%2").arg(pid).arg(name);
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
// attachViaPlugin does not set tree.baseAddress; set it from the new provider (like selectSource does).
|
||||
if (doc->provider && doc->provider->base() != 0) {
|
||||
doc->tree.baseAddress = doc->provider->base();
|
||||
doc->tree.baseAddressFormula.clear();
|
||||
ctrl->refresh();
|
||||
}
|
||||
return makeTextResult("Attached to process " + name + " (PID " + QString::number(pid) + ")");
|
||||
}
|
||||
|
||||
@@ -986,6 +1090,54 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
||||
return makeTextResult("Provide sourceIndex, filePath, or pid", true);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: source.modules
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolSourceModules(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab", true);
|
||||
|
||||
auto* prov = tab->doc->provider.get();
|
||||
if (!prov) return makeTextResult("No data source attached", true);
|
||||
|
||||
QVector<MemoryRegion> regions = prov->enumerateRegions();
|
||||
// Build unique modules: name -> { minBase, maxEnd }
|
||||
QHash<QString, QPair<uint64_t, uint64_t>> moduleMap;
|
||||
for (const auto& r : regions) {
|
||||
if (r.moduleName.isEmpty()) continue;
|
||||
uint64_t end = r.base + r.size;
|
||||
auto it = moduleMap.find(r.moduleName);
|
||||
if (it == moduleMap.end()) {
|
||||
moduleMap[r.moduleName] = qMakePair(r.base, end);
|
||||
} else {
|
||||
it->first = qMin(it->first, r.base);
|
||||
it->second = qMax(it->second, end);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonArray arr;
|
||||
QStringList names = moduleMap.keys();
|
||||
std::sort(names.begin(), names.end(), [](const QString& a, const QString& b) {
|
||||
return QString::compare(a, b, Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
for (const QString& name : names) {
|
||||
const auto& p = moduleMap[name];
|
||||
uint64_t base = p.first;
|
||||
uint64_t size = p.second - p.first;
|
||||
arr.append(QJsonObject{
|
||||
{"name", name},
|
||||
{"base", "0x" + QString::number(base, 16).toUpper()},
|
||||
{"size", QJsonValue(static_cast<qint64>(size))}
|
||||
});
|
||||
}
|
||||
|
||||
QJsonObject out;
|
||||
out["modules"] = arr;
|
||||
out["count"] = arr.size();
|
||||
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: hex.read
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
@@ -997,12 +1149,19 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
|
||||
auto* prov = tab->doc->provider.get();
|
||||
if (!prov) return makeTextResult("No provider", true);
|
||||
|
||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||
int length = qMin(args.value("length").toInt(64), 4096);
|
||||
int64_t offset = parseInteger(args.value("offset"));
|
||||
int length = qBound(1, (int)parseInteger(args.value("length"), 64), 4096);
|
||||
bool baseRel = args.value("baseRelative").toBool();
|
||||
|
||||
if (!args.value("baseRelative").toBool())
|
||||
if (baseRel)
|
||||
offset += (int64_t)tab->doc->tree.baseAddress;
|
||||
|
||||
qDebug() << "[hex_read] arg offset" << (args.value("offset").isString() ? "str" : "num")
|
||||
<< (args.value("offset").isString() ? args.value("offset").toString() : QString())
|
||||
<< Qt::showbase << Qt::hex << (quint64)offset
|
||||
<< "baseRelative" << baseRel << "tree.base" << (quint64)tab->doc->tree.baseAddress
|
||||
<< "final addr" << (quint64)offset << Qt::dec;
|
||||
|
||||
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
|
||||
return makeTextResult("Cannot read at offset " + QString::number(offset), true);
|
||||
|
||||
@@ -1079,10 +1238,10 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
|
||||
auto* doc = tab->doc;
|
||||
auto* prov = doc->provider.get();
|
||||
|
||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||
int64_t offset = parseInteger(args.value("offset"));
|
||||
QString hexStr = args.value("hexBytes").toString().remove(' ');
|
||||
|
||||
if (!args.value("baseRelative").toBool())
|
||||
if (args.value("baseRelative").toBool())
|
||||
offset += (int64_t)doc->tree.baseAddress;
|
||||
|
||||
if (hexStr.size() % 2 != 0)
|
||||
@@ -1266,7 +1425,7 @@ QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) {
|
||||
const auto& tree = tab->doc->tree;
|
||||
QString query = args.value("query").toString();
|
||||
QString kindFilter = args.value("kindFilter").toString();
|
||||
int limit = qBound(1, args.value("limit").toInt(20), 100);
|
||||
int limit = qBound(1, (int)parseInteger(args.value("limit"), 20), 100);
|
||||
|
||||
if (query.isEmpty() && kindFilter.isEmpty())
|
||||
return makeTextResult("Provide 'query' (name substring) and/or 'kindFilter' (e.g. 'Struct')", true);
|
||||
@@ -1356,37 +1515,167 @@ QJsonObject McpBridge::toolNodeHistory(const QJsonObject& args) {
|
||||
QJsonDocument(result).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: process.info — PEB address + TEB enumeration
|
||||
// TOOL: scanner.scan
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
|
||||
static ValueType valueTypeFromString(const QString& s) {
|
||||
QString lower = s.trimmed().toLower();
|
||||
if (lower == QStringLiteral("int8")) return ValueType::Int8;
|
||||
if (lower == QStringLiteral("int16")) return ValueType::Int16;
|
||||
if (lower == QStringLiteral("int32")) return ValueType::Int32;
|
||||
if (lower == QStringLiteral("int64")) return ValueType::Int64;
|
||||
if (lower == QStringLiteral("uint8")) return ValueType::UInt8;
|
||||
if (lower == QStringLiteral("uint16")) return ValueType::UInt16;
|
||||
if (lower == QStringLiteral("uint32")) return ValueType::UInt32;
|
||||
if (lower == QStringLiteral("uint64")) return ValueType::UInt64;
|
||||
if (lower == QStringLiteral("float")) return ValueType::Float;
|
||||
if (lower == QStringLiteral("double")) return ValueType::Double;
|
||||
return ValueType::Float; // default
|
||||
}
|
||||
|
||||
static QVector<AddressRange> parseRegionsArg(const QJsonObject& args, QString* errOut = nullptr) {
|
||||
QVector<AddressRange> out;
|
||||
QJsonArray arr = args.value("regions").toArray();
|
||||
if (arr.isEmpty()) return out;
|
||||
out.reserve(arr.size());
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
QJsonArray pair = arr[i].toArray();
|
||||
if (pair.size() != 2) {
|
||||
if (errOut) *errOut = QStringLiteral("regions[%1]: expected [startHex, endHex]").arg(i);
|
||||
return {};
|
||||
}
|
||||
bool ok1 = false, ok2 = false;
|
||||
uint64_t start = pair[0].toString().toULongLong(&ok1, 0);
|
||||
uint64_t end = pair[1].toString().toULongLong(&ok2, 0);
|
||||
if (!ok1 || !ok2) {
|
||||
if (errOut) *errOut = QStringLiteral("regions[%1]: invalid hex address").arg(i);
|
||||
return {};
|
||||
}
|
||||
if (end <= start) {
|
||||
if (errOut) *errOut = QStringLiteral("regions[%1]: end must be > start").arg(i);
|
||||
return {};
|
||||
}
|
||||
out.append({start, end});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QJsonObject McpBridge::toolScannerScan(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab", true);
|
||||
|
||||
auto* prov = tab->doc->provider.get();
|
||||
if (!prov) return makeTextResult("No data source attached", true);
|
||||
if (!prov->isLive()) return makeTextResult("Not a live provider", true);
|
||||
ScannerPanel* panel = m_mainWindow->m_scannerPanel;
|
||||
if (!panel) return makeTextResult("Scanner panel not available", true);
|
||||
|
||||
uint64_t pebAddr = prov->peb();
|
||||
if (!pebAddr) return makeTextResult("PEB not available for this provider", true);
|
||||
QString valueTypeStr = args.value("valueType").toString();
|
||||
QString value = args.value("value").toString();
|
||||
bool filterExec = args.value("filterExecutable").toBool();
|
||||
bool filterWrite = args.value("filterWritable").toBool();
|
||||
|
||||
QJsonObject out;
|
||||
out["peb"] = "0x" + QString::number(pebAddr, 16).toUpper();
|
||||
if (value.isEmpty())
|
||||
return makeTextResult("Missing 'value' (e.g. \"120\")", true);
|
||||
|
||||
auto tebList = prov->tebs();
|
||||
QJsonArray tebArr;
|
||||
for (const auto& t : tebList) {
|
||||
tebArr.append(QJsonObject{
|
||||
{"address", "0x" + QString::number(t.tebAddress, 16).toUpper()},
|
||||
{"threadId", (qint64)t.threadId}
|
||||
});
|
||||
QString regErr;
|
||||
auto constrainRegions = parseRegionsArg(args, ®Err);
|
||||
if (!regErr.isEmpty())
|
||||
return makeTextResult(regErr, true);
|
||||
|
||||
ValueType vt = valueTypeFromString(valueTypeStr);
|
||||
QVector<ScanResult> results = panel->runValueScanAndWait(vt, value, filterExec, filterWrite, constrainRegions);
|
||||
|
||||
QString msg = QStringLiteral("Scan (%1 = %2): %3 result(s).")
|
||||
.arg(valueTypeStr.isEmpty() ? QStringLiteral("float") : valueTypeStr)
|
||||
.arg(value)
|
||||
.arg(results.size());
|
||||
if (!constrainRegions.isEmpty()) {
|
||||
uint64_t totalConstrained = 0;
|
||||
for (const auto& r : constrainRegions) totalConstrained += r.end - r.start;
|
||||
msg += QStringLiteral("\nRegion constraint: %1 range(s), %2 bytes total requested.")
|
||||
.arg(constrainRegions.size()).arg(totalConstrained);
|
||||
}
|
||||
const int showAddrs = 15;
|
||||
if (!results.isEmpty()) {
|
||||
msg += QStringLiteral("\nFirst addresses:");
|
||||
for (int i = 0; i < qMin(results.size(), showAddrs); i++) {
|
||||
msg += QStringLiteral("\n 0x%1").arg(results[i].address, 16, 16, QChar('0'));
|
||||
if (!results[i].regionModule.isEmpty())
|
||||
msg += QStringLiteral(" (%1)").arg(results[i].regionModule);
|
||||
}
|
||||
if (results.size() > showAddrs)
|
||||
msg += QStringLiteral("\n ... and %1 more").arg(results.size() - showAddrs);
|
||||
}
|
||||
return makeTextResult(msg);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: scanner.scan_pattern
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolScannerScanPattern(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab", true);
|
||||
|
||||
ScannerPanel* panel = m_mainWindow->m_scannerPanel;
|
||||
if (!panel) return makeTextResult("Scanner panel not available", true);
|
||||
|
||||
QString pattern = args.value("pattern").toString().trimmed();
|
||||
bool filterExec = args.value("filterExecutable").toBool();
|
||||
bool filterWrite = args.value("filterWritable").toBool();
|
||||
|
||||
if (pattern.isEmpty())
|
||||
return makeTextResult("Missing 'pattern' (e.g. \"00 00 20 42 00 00 20 42\")", true);
|
||||
|
||||
QString regErr;
|
||||
auto constrainRegions = parseRegionsArg(args, ®Err);
|
||||
if (!regErr.isEmpty())
|
||||
return makeTextResult(regErr, true);
|
||||
|
||||
// Use the resolved tab's provider so the scan runs on the same tab we attached to (source_switch).
|
||||
// If we used the panel's default getter we'd get the *active* tab's provider, which may be different.
|
||||
std::shared_ptr<rcx::Provider> provider = (tab->doc && tab->doc->provider) ? tab->doc->provider : nullptr;
|
||||
if (!provider) {
|
||||
return makeTextResult("No provider on this tab — the scan did not run. Use source_switch to attach to a process (or open a file), then run the pattern scan again. If you already ran source_switch, ensure the tab that was switched is the one used (e.g. pass tabIndex: 0 for the first tab).", true);
|
||||
}
|
||||
|
||||
out["tebs"] = tebArr;
|
||||
out["tebCount"] = tebArr.size();
|
||||
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
|
||||
QVector<ScanResult> results = panel->runPatternScanAndWait(provider, pattern, filterExec, filterWrite, constrainRegions);
|
||||
|
||||
QString msg = QStringLiteral("Pattern scan (%1): %2 result(s).")
|
||||
.arg(pattern)
|
||||
.arg(results.size());
|
||||
if (!constrainRegions.isEmpty()) {
|
||||
uint64_t totalConstrained = 0;
|
||||
for (const auto& r : constrainRegions) totalConstrained += r.end - r.start;
|
||||
msg += QStringLiteral("\nRegion constraint: %1 range(s), %2 bytes total requested.")
|
||||
.arg(constrainRegions.size()).arg(totalConstrained);
|
||||
}
|
||||
const int showAddrs = 15;
|
||||
if (!results.isEmpty()) {
|
||||
msg += QStringLiteral("\nFirst addresses:");
|
||||
for (int i = 0; i < qMin(results.size(), showAddrs); i++) {
|
||||
msg += QStringLiteral("\n 0x%1").arg(results[i].address, 16, 16, QChar('0'));
|
||||
if (!results[i].regionModule.isEmpty())
|
||||
msg += QStringLiteral(" (%1)").arg(results[i].regionModule);
|
||||
}
|
||||
if (results.size() > showAddrs)
|
||||
msg += QStringLiteral("\n ... and %1 more").arg(results.size() - showAddrs);
|
||||
}
|
||||
return makeTextResult(msg);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: mcp.reconnect
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolReconnect(const QJsonObject&) {
|
||||
if (!m_client)
|
||||
return makeTextResult("No client connected.", true);
|
||||
// Disconnect after this response is sent so the client receives the result
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
if (m_client) {
|
||||
m_client->disconnectFromServer();
|
||||
}
|
||||
});
|
||||
return makeTextResult("Disconnected. The MCP client will exit; your IDE may restart it and reconnect to Reclass.");
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -54,13 +54,16 @@ private:
|
||||
QJsonObject toolProjectState(const QJsonObject& args);
|
||||
QJsonObject toolTreeApply(const QJsonObject& args);
|
||||
QJsonObject toolSourceSwitch(const QJsonObject& args);
|
||||
QJsonObject toolSourceModules(const QJsonObject& args);
|
||||
QJsonObject toolHexRead(const QJsonObject& args);
|
||||
QJsonObject toolHexWrite(const QJsonObject& args);
|
||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
QJsonObject toolNodeHistory(const QJsonObject& args);
|
||||
QJsonObject toolProcessInfo(const QJsonObject& args);
|
||||
QJsonObject toolScannerScan(const QJsonObject& args);
|
||||
QJsonObject toolScannerScanPattern(const QJsonObject& args);
|
||||
QJsonObject toolReconnect(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
|
||||
@@ -492,6 +492,41 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) &&
|
||||
req.endAddress > req.startAddress;
|
||||
|
||||
// If constrainRegions specified, intersect with provider regions
|
||||
if (!req.constrainRegions.isEmpty()) {
|
||||
// Sort and merge overlapping/adjacent constraints to avoid duplicate sub-regions
|
||||
auto constraints = req.constrainRegions;
|
||||
std::sort(constraints.begin(), constraints.end(),
|
||||
[](const AddressRange& a, const AddressRange& b) { return a.start < b.start; });
|
||||
QVector<AddressRange> merged;
|
||||
for (const auto& c : constraints) {
|
||||
if (c.end <= c.start) continue; // skip degenerate ranges
|
||||
if (!merged.isEmpty() && c.start <= merged.last().end)
|
||||
merged.last().end = qMax(merged.last().end, c.end);
|
||||
else
|
||||
merged.append(c);
|
||||
}
|
||||
|
||||
QVector<MemoryRegion> clipped;
|
||||
for (const auto& region : regions) {
|
||||
uint64_t rEnd = region.base + region.size;
|
||||
for (const auto& c : merged) {
|
||||
if (c.end <= region.base || c.start >= rEnd) continue;
|
||||
uint64_t iStart = qMax(region.base, c.start);
|
||||
uint64_t iEnd = qMin(rEnd, c.end);
|
||||
if (iEnd <= iStart) continue;
|
||||
MemoryRegion sub = region;
|
||||
sub.base = iStart;
|
||||
sub.size = iEnd - iStart;
|
||||
clipped.append(sub);
|
||||
}
|
||||
}
|
||||
regions = std::move(clipped);
|
||||
qDebug() << "[scan] constrained to" << regions.size() << "sub-regions from"
|
||||
<< req.constrainRegions.size() << "address ranges ("
|
||||
<< merged.size() << "after merge)";
|
||||
}
|
||||
|
||||
// Pre-compute total bytes for progress
|
||||
uint64_t totalBytes = 0;
|
||||
for (const auto& r : regions) {
|
||||
@@ -541,7 +576,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
continue;
|
||||
}
|
||||
|
||||
const int overlap = patternLen; // need full patternLen overlap so pattern at chunk end is found
|
||||
const int overlap = patternLen - 1;
|
||||
QByteArray chunk(qMin((uint64_t)kChunk, regSize), Qt::Uninitialized);
|
||||
uint64_t regOffset = regStart - region.base; // offset within provider region
|
||||
|
||||
@@ -597,9 +632,12 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
}
|
||||
}
|
||||
|
||||
// Advance with overlap to catch patterns that straddle chunks
|
||||
// Advance with overlap to catch patterns that straddle chunks.
|
||||
// Skip overlap on the final chunk -- nothing follows to overlap into.
|
||||
uint64_t advance;
|
||||
if (readLen > overlap)
|
||||
if ((uint64_t)readLen >= remaining)
|
||||
advance = remaining; // last chunk, no overlap needed
|
||||
else if (readLen > overlap)
|
||||
advance = (uint64_t)(readLen - overlap);
|
||||
else
|
||||
advance = 1; // prevent infinite loop on tiny regions
|
||||
|
||||
@@ -34,6 +34,11 @@ enum class ScanCondition {
|
||||
|
||||
// ── Scan request / result ──
|
||||
|
||||
struct AddressRange {
|
||||
uint64_t start = 0;
|
||||
uint64_t end = 0; // exclusive
|
||||
};
|
||||
|
||||
struct ScanRequest {
|
||||
QByteArray pattern; // literal bytes to match (empty for UnknownValue)
|
||||
QByteArray mask; // 0xFF = must match, 0x00 = wildcard
|
||||
@@ -49,6 +54,9 @@ struct ScanRequest {
|
||||
|
||||
uint64_t startAddress = 0; // 0 = no limit (scan all regions)
|
||||
uint64_t endAddress = 0; // 0 = no limit (scan all regions)
|
||||
|
||||
// If non-empty, only scan within these address ranges (intersected with provider regions).
|
||||
QVector<AddressRange> constrainRegions;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QEventLoop>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -418,6 +419,98 @@ ScanRequest ScannerPanel::buildRequest() {
|
||||
return req;
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScannerPanel::runValueScanAndWait(ValueType valueType, const QString& value,
|
||||
bool filterExecutable, bool filterWritable,
|
||||
const QVector<AddressRange>& constrainRegions) {
|
||||
QVector<ScanResult> results;
|
||||
QString err;
|
||||
ScanRequest req;
|
||||
if (!serializeValue(valueType, value, req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return results;
|
||||
}
|
||||
req.alignment = naturalAlignment(valueType);
|
||||
req.filterExecutable = filterExecutable;
|
||||
req.filterWritable = filterWritable;
|
||||
req.constrainRegions = constrainRegions;
|
||||
|
||||
auto provider = m_providerGetter ? m_providerGetter() : nullptr;
|
||||
if (!provider) {
|
||||
m_statusLabel->setText(QStringLiteral("No provider (attach to a process or open a file first)"));
|
||||
return results;
|
||||
}
|
||||
if (m_engine->isRunning()) {
|
||||
m_statusLabel->setText(QStringLiteral("Scan already in progress"));
|
||||
return results;
|
||||
}
|
||||
|
||||
m_lastScanMode = 1;
|
||||
m_lastValueType = valueType;
|
||||
m_lastPattern = req.pattern;
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
m_statusLabel->setText(QStringLiteral("Scanning..."));
|
||||
|
||||
QEventLoop loop;
|
||||
connect(m_engine, &ScanEngine::finished, this, [&results, &loop](const QVector<ScanResult>& r) {
|
||||
results = r;
|
||||
loop.quit();
|
||||
}, Qt::SingleShotConnection);
|
||||
m_engine->start(provider, req);
|
||||
loop.exec();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScannerPanel::runPatternScanAndWait(const QString& pattern,
|
||||
bool filterExecutable, bool filterWritable,
|
||||
const QVector<AddressRange>& constrainRegions) {
|
||||
auto provider = m_providerGetter ? m_providerGetter() : nullptr;
|
||||
return runPatternScanAndWait(provider, pattern, filterExecutable, filterWritable, constrainRegions);
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScannerPanel::runPatternScanAndWait(std::shared_ptr<Provider> provider,
|
||||
const QString& pattern,
|
||||
bool filterExecutable, bool filterWritable,
|
||||
const QVector<AddressRange>& constrainRegions) {
|
||||
QVector<ScanResult> results;
|
||||
QString err;
|
||||
ScanRequest req;
|
||||
if (!parseSignature(pattern, req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return results;
|
||||
}
|
||||
req.alignment = 1;
|
||||
req.filterExecutable = filterExecutable;
|
||||
req.filterWritable = filterWritable;
|
||||
req.constrainRegions = constrainRegions;
|
||||
|
||||
if (!provider) {
|
||||
m_statusLabel->setText(QStringLiteral("No provider (attach to a process or open a file first)"));
|
||||
return results;
|
||||
}
|
||||
if (m_engine->isRunning()) {
|
||||
m_statusLabel->setText(QStringLiteral("Scan already in progress"));
|
||||
return results;
|
||||
}
|
||||
|
||||
m_lastScanMode = 0;
|
||||
m_lastPattern = req.pattern;
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
m_statusLabel->setText(QStringLiteral("Scanning..."));
|
||||
|
||||
QEventLoop loop;
|
||||
connect(m_engine, &ScanEngine::finished, this, [&results, &loop](const QVector<ScanResult>& r) {
|
||||
results = r;
|
||||
loop.quit();
|
||||
}, Qt::SingleShotConnection);
|
||||
m_engine->start(provider, req);
|
||||
loop.exec();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
|
||||
@@ -60,6 +60,21 @@ public:
|
||||
QLabel* condLabel() const { return m_condLabel; }
|
||||
QCheckBox* structOnlyCheck() const { return m_structOnlyCheck; }
|
||||
|
||||
/** Run a value scan and block until done. For MCP / automation. Returns results; updates panel table. */
|
||||
QVector<ScanResult> runValueScanAndWait(ValueType valueType, const QString& value,
|
||||
bool filterExecutable = false, bool filterWritable = false,
|
||||
const QVector<AddressRange>& constrainRegions = {});
|
||||
|
||||
/** Run a pattern/signature scan and block until done. Pattern: space-separated hex bytes, e.g. "00 00 20 42 ?? ??". */
|
||||
QVector<ScanResult> runPatternScanAndWait(const QString& pattern,
|
||||
bool filterExecutable = false, bool filterWritable = false,
|
||||
const QVector<AddressRange>& constrainRegions = {});
|
||||
|
||||
/** Run pattern scan using the given provider (for MCP: use tab's provider so scan runs on the right tab). */
|
||||
QVector<ScanResult> runPatternScanAndWait(std::shared_ptr<Provider> provider, const QString& pattern,
|
||||
bool filterExecutable = false, bool filterWritable = false,
|
||||
const QVector<AddressRange>& constrainRegions = {});
|
||||
|
||||
signals:
|
||||
void goToAddress(uint64_t address);
|
||||
|
||||
|
||||
@@ -1186,6 +1186,813 @@ private slots:
|
||||
QCOMPARE(results[0].address, (uint64_t)8);
|
||||
QCOMPARE(results[3].address, (uint64_t)20);
|
||||
}
|
||||
|
||||
// -- constrainRegions (multi-range intersection) --
|
||||
|
||||
void scan_constrainRegions_multipleRanges() {
|
||||
QByteArray data(32, 0);
|
||||
data[4] = char(0xBB);
|
||||
data[12] = char(0xBB);
|
||||
data[20] = char(0xBB);
|
||||
data[28] = char(0xBB);
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\xBB", 1);
|
||||
req.mask = QByteArray("\xFF", 1);
|
||||
req.constrainRegions = {{0, 8}, {16, 24}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
QCOMPARE(results[0].address, (uint64_t)4);
|
||||
QCOMPARE(results[1].address, (uint64_t)20);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_intersectsProviderRegions() {
|
||||
QByteArray data(256, 0);
|
||||
data[160] = char(0xCC);
|
||||
data[210] = char(0xCC);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({100, 100, true, false, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\xCC", 1);
|
||||
req.mask = QByteArray("\xFF", 1);
|
||||
req.constrainRegions = {{150, 250}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)160);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_noOverlap() {
|
||||
QByteArray data(32, char(0xEE));
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 16, true, false, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\xEE", 1);
|
||||
req.mask = QByteArray("\xFF", 1);
|
||||
req.constrainRegions = {{100, 200}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 0);
|
||||
}
|
||||
|
||||
// -- constrainRegions edge cases --
|
||||
|
||||
void scan_constrainRegions_gapBetweenRegions() {
|
||||
// Provider has two regions with a gap: [0,16) and [32,48).
|
||||
// Constraint spans the gap: [8, 40). Should find matches in both.
|
||||
QByteArray data(64, 0);
|
||||
data[10] = char(0xDD);
|
||||
data[35] = char(0xDD);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 16, true, true, false, {}});
|
||||
regions.append({32, 16, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xDD));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{8, 40}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
QCOMPARE(results[0].address, (uint64_t)10);
|
||||
QCOMPARE(results[1].address, (uint64_t)35);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_partialRegionOverlap() {
|
||||
// Provider region [100, 200). Constraint [150, 250) clips to [150, 200).
|
||||
QByteArray data(256, 0);
|
||||
data[120] = char(0xAB);
|
||||
data[160] = char(0xAB);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({100, 100, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xAB));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{150, 250}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)160);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_mixedModuleAndAnonymous() {
|
||||
// Module region + anonymous heap region. Constraint covers both.
|
||||
QByteArray data(0x10000, 0);
|
||||
data[0x1500] = char(0xCC);
|
||||
data[0x5500] = char(0xCC);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0x1000, 0x1000, true, false, true, QString("game.exe")});
|
||||
regions.append({0x5000, 0x1000, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xCC));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{0x0, 0x10000}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
QCOMPARE(results[0].address, (uint64_t)0x1500);
|
||||
QCOMPARE(results[1].address, (uint64_t)0x5500);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_fallbackProvider() {
|
||||
// BufferProvider returns no regions -> fallback [0, size).
|
||||
// constrainRegions should still work against the fallback.
|
||||
QByteArray data(64, 0);
|
||||
data[10] = char(0xAA);
|
||||
data[30] = char(0xAA);
|
||||
data[50] = char(0xAA);
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xAA));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{5, 35}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
QCOMPARE(results[0].address, (uint64_t)10);
|
||||
QCOMPARE(results[1].address, (uint64_t)30);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_adjacentRegions() {
|
||||
// Two adjacent regions [0,16) and [16,32). Constraint [8,24) spans both.
|
||||
QByteArray data(32, 0);
|
||||
data[12] = char(0xEF);
|
||||
data[20] = char(0xEF);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 16, true, true, false, {}});
|
||||
regions.append({16, 16, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xEF));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{8, 24}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
QCOMPARE(results[0].address, (uint64_t)12);
|
||||
QCOMPARE(results[1].address, (uint64_t)20);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_writableFilterPreserved() {
|
||||
// filterWritable=true should still exclude non-writable clipped regions.
|
||||
QByteArray data(0x4000, 0);
|
||||
data[0x1100] = char(0xBB);
|
||||
data[0x2100] = char(0xBB);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0x1000, 0x1000, true, false, true, {}});
|
||||
regions.append({0x2000, 0x1000, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xBB));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.filterWritable = true;
|
||||
req.constrainRegions = {{0x1000, 0x3000}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)0x2100);
|
||||
}
|
||||
|
||||
|
||||
void scan_constrainRegions_constraintExtendsBeforeAndAfter() {
|
||||
// Region [10, 20). Constraint [0, 30) extends before and after.
|
||||
// Should only scan [10, 20) — the intersection.
|
||||
QByteArray data(32, 0);
|
||||
data[5] = char(0xAA); // outside region, should NOT be found
|
||||
data[15] = char(0xAA); // inside region, should be found
|
||||
data[25] = char(0xAA); // outside region, should NOT be found
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({10, 10, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xAA));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{0, 30}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)15);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_emptyConstraintScansAll() {
|
||||
// Empty constrainRegions should scan everything (no restriction).
|
||||
QByteArray data(32, 0);
|
||||
data[5] = char(0xBB);
|
||||
data[15] = char(0xBB);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 32, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xBB));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
// constrainRegions left empty
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_singleAddressRange() {
|
||||
// Equivalent to startAddress/endAddress: single constraint range.
|
||||
QByteArray data(32, 0);
|
||||
data[8] = char(0xAA);
|
||||
data[16] = char(0xAA);
|
||||
data[24] = char(0xAA);
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xAA));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{8, 20}}; // same as startAddress=8, endAddress=20
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
QCOMPARE(results[0].address, (uint64_t)8);
|
||||
QCOMPARE(results[1].address, (uint64_t)16);
|
||||
}
|
||||
|
||||
|
||||
void scan_constrainRegions_withStartEndAddress() {
|
||||
// Both constrainRegions and startAddress/endAddress set.
|
||||
// constrainRegions: [0, 16) and [24, 32). startAddress/endAddress: [8, 28).
|
||||
// Effective scan should be intersection of both: [8, 16) and [24, 28).
|
||||
// Match at 4 (outside both), 12 (in both), 20 (in startEnd but not constrain),
|
||||
// 26 (in both), 30 (in constrain but not startEnd).
|
||||
QByteArray data(32, 0);
|
||||
data[4] = char(0xDD);
|
||||
data[12] = char(0xDD);
|
||||
data[20] = char(0xDD);
|
||||
data[26] = char(0xDD);
|
||||
data[30] = char(0xDD);
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xDD));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{0, 16}, {24, 32}};
|
||||
req.startAddress = 8;
|
||||
req.endAddress = 28;
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2); // only 12 and 26
|
||||
QCOMPARE(results[0].address, (uint64_t)12);
|
||||
QCOMPARE(results[1].address, (uint64_t)26);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_unknownValueScan() {
|
||||
// Unknown value scan with constrainRegions should only capture within ranges.
|
||||
QByteArray data(32, char(0x42));
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.condition = ScanCondition::UnknownValue;
|
||||
req.valueSize = 4;
|
||||
req.alignment = 4;
|
||||
req.constrainRegions = {{8, 24}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
// Range [8, 24) = 16 bytes, alignment 4, valueSize 4 -> offsets 8, 12, 16, 20 = 4 results
|
||||
QCOMPARE(results.size(), 4);
|
||||
QCOMPARE(results[0].address, (uint64_t)8);
|
||||
QCOMPARE(results[3].address, (uint64_t)20);
|
||||
}
|
||||
|
||||
|
||||
void scan_constrainRegions_nonZeroBase() {
|
||||
// Region with non-zero base; constraint matches exactly.
|
||||
QByteArray data(0x10000, 0);
|
||||
data[0x8100] = char(0xFF);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0x8000, 0x1000, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data2, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xFF));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{0x8000, 0x9000}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)0x8100);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_zeroSizeConstraint() {
|
||||
// Degenerate: constraint with start == end (zero size). Should scan nothing.
|
||||
QByteArray data(32, char(0xAA));
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xAA));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{10, 10}}; // zero-size
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 0);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_invertedRange() {
|
||||
// Degenerate: constraint with start > end. Should be treated as empty/invalid.
|
||||
QByteArray data(32, char(0xAA));
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xAA));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{20, 10}}; // inverted
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 0);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_overlappingConstraints() {
|
||||
// Two overlapping constraints: [4, 20) and [12, 28).
|
||||
// Should NOT double-count matches in the overlap [12, 20).
|
||||
QByteArray data(32, 0);
|
||||
data[8] = char(0xCC);
|
||||
data[16] = char(0xCC);
|
||||
data[24] = char(0xCC);
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xCC));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{4, 20}, {12, 28}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
// After merge: [4, 28). All three matches are in range, no duplicates.
|
||||
QCOMPARE(results.size(), 3);
|
||||
}
|
||||
|
||||
|
||||
void scan_constrainRegions_patternAtFirstByte() {
|
||||
// Pattern at the very first byte of a clipped sub-region.
|
||||
// Region [0, 64). Constraint [20, 40). Match at offset 20.
|
||||
QByteArray data(64, 0);
|
||||
data[20] = char(0xFE);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 64, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0xFE));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{20, 40}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)20);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_patternAtLastByte() {
|
||||
// Pattern at the very last valid position of a clipped sub-region.
|
||||
// Region [0, 64). Constraint [20, 40). 4-byte pattern at offset 36 (last valid: 40-4=36).
|
||||
QByteArray data(64, 0);
|
||||
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 64, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4);
|
||||
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
|
||||
req.constrainRegions = {{20, 40}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)36);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_patternOneByteAfterEnd() {
|
||||
// Pattern starts 1 byte before constraint end — only 3 of 4 bytes are in range.
|
||||
// Should NOT match because the full pattern doesn't fit.
|
||||
// Region [0, 64). Constraint [20, 39). 4-byte pattern at offset 36 (needs 36..39, but 39 is excluded).
|
||||
QByteArray data(64, 0);
|
||||
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 64, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4);
|
||||
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
|
||||
req.constrainRegions = {{20, 39}}; // ends at 39, pattern needs 36..39 inclusive
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 0); // pattern doesn't fit
|
||||
}
|
||||
|
||||
void scan_constrainRegions_regionSmallerThanPattern() {
|
||||
// Clipped sub-region is smaller than the pattern. Should scan nothing, not crash.
|
||||
// Region [0, 64). Constraint [30, 32). 4-byte pattern can't fit in 2 bytes.
|
||||
QByteArray data(64, char(0xAA));
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 64, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\xAA\xAA\xAA\xAA", 4);
|
||||
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
|
||||
req.constrainRegions = {{30, 32}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 0);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_patternExactlyFitsRegion() {
|
||||
// Clipped sub-region is exactly pattern size. Should find match if bytes match.
|
||||
// Region [0, 64). Constraint [30, 34). 4-byte pattern, 4-byte region.
|
||||
QByteArray data(64, 0);
|
||||
data[30] = char(0x11); data[31] = char(0x22); data[32] = char(0x33); data[33] = char(0x44);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 64, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\x11\x22\x33\x44", 4);
|
||||
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
|
||||
req.constrainRegions = {{30, 34}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 1);
|
||||
QCOMPARE(results[0].address, (uint64_t)30);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_matchAtRegionBoundaries() {
|
||||
// Two adjacent clipped sub-regions. Matches at the last byte of the first
|
||||
// and first byte of the second. Both should be found.
|
||||
// Regions: [0, 16) and [16, 32). Constraint [0, 32) (full coverage).
|
||||
QByteArray data(32, 0);
|
||||
data[15] = char(0x77); // last byte of first region
|
||||
data[16] = char(0x77); // first byte of second region
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 16, true, true, false, {}});
|
||||
regions.append({16, 16, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray(1, char(0x77));
|
||||
req.mask = QByteArray(1, char(0xFF));
|
||||
req.constrainRegions = {{0, 32}};
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 2);
|
||||
QCOMPARE(results[0].address, (uint64_t)15);
|
||||
QCOMPARE(results[1].address, (uint64_t)16);
|
||||
}
|
||||
|
||||
void scan_constrainRegions_multibyteAtClipBoundary() {
|
||||
// 4-byte pattern that straddles the constraint boundary — should NOT be found
|
||||
// because the clipped region doesn't contain the full pattern.
|
||||
// Region [0, 64). Constraint [10, 13). Pattern at offset 10 is 4 bytes (10..13),
|
||||
// but constraint end is 13 (exclusive), so only 3 bytes [10,13) are in range.
|
||||
QByteArray data(64, 0);
|
||||
data[10] = char(0xAA); data[11] = char(0xBB); data[12] = char(0xCC); data[13] = char(0xDD);
|
||||
QVector<MemoryRegion> regions;
|
||||
regions.append({0, 64, true, true, false, {}});
|
||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = QByteArray("\xAA\xBB\xCC\xDD", 4);
|
||||
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
|
||||
req.constrainRegions = {{10, 13}}; // only 3 bytes, pattern needs 4
|
||||
engine.start(prov, req);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QCOMPARE(results.size(), 0);
|
||||
}
|
||||
|
||||
|
||||
// ── Value type + pattern scans at every position in a constrained region ──
|
||||
|
||||
// Helper: run a scan with the given pattern/mask/alignment in a constrained region,
|
||||
// return the result addresses.
|
||||
QVector<uint64_t> scanConstrained(const QByteArray& data,
|
||||
const QByteArray& pat, const QByteArray& mask,
|
||||
int alignment, uint64_t cStart, uint64_t cEnd) {
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
ScanEngine engine;
|
||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||
ScanRequest req;
|
||||
req.pattern = pat;
|
||||
req.mask = mask;
|
||||
req.alignment = alignment;
|
||||
req.constrainRegions = {{cStart, cEnd}};
|
||||
engine.start(prov, req);
|
||||
if (!finSpy.wait(5000)) return {};
|
||||
auto results = finSpy.first().first().value<QVector<ScanResult>>();
|
||||
QVector<uint64_t> addrs;
|
||||
for (const auto& r : results) addrs.append(r.address);
|
||||
return addrs;
|
||||
}
|
||||
|
||||
void scan_int32_atRegionStart() {
|
||||
QByteArray data(128, 0);
|
||||
int32_t v = 0x12345678;
|
||||
std::memcpy(data.data() + 32, &v, 4);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Int32, "305419896", pat, mask)); // 0x12345678
|
||||
auto addrs = scanConstrained(data, pat, mask, 4, 32, 96);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)32);
|
||||
}
|
||||
|
||||
void scan_int32_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
int32_t v = 0x12345678;
|
||||
// Last aligned 4-byte position in [32, 96) is 92
|
||||
std::memcpy(data.data() + 92, &v, 4);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Int32, "305419896", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 4, 32, 96);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)92);
|
||||
}
|
||||
|
||||
void scan_float_atRegionStart() {
|
||||
QByteArray data(128, 0);
|
||||
float v = 3.14f;
|
||||
std::memcpy(data.data() + 16, &v, 4);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 4, 16, 80);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)16);
|
||||
}
|
||||
|
||||
void scan_float_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
float v = 3.14f;
|
||||
// Last aligned 4-byte position in [16, 80) is 76
|
||||
std::memcpy(data.data() + 76, &v, 4);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 4, 16, 80);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)76);
|
||||
}
|
||||
|
||||
void scan_double_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
double v = 2.71828;
|
||||
// Last aligned 8-byte position in [0, 128) is 120
|
||||
std::memcpy(data.data() + 120, &v, 8);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Double, "2.71828", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 8, 0, 128);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)120);
|
||||
}
|
||||
|
||||
void scan_int64_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
int64_t v = 0x0BADC0DEDEADBEEFLL;
|
||||
// Last aligned 8-byte position in [8, 72) is 64
|
||||
std::memcpy(data.data() + 64, &v, 8);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Int64, "841540768839352047", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 8, 8, 72);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)64);
|
||||
}
|
||||
|
||||
void scan_utf16_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
// "AB" in UTF-16LE = 4 bytes
|
||||
uint16_t chars[] = { 'A', 'B' };
|
||||
// Last aligned 2-byte position where 4 bytes fit in [0, 128) is 124
|
||||
std::memcpy(data.data() + 124, chars, 4);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::UTF16, "AB", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 2, 0, 128);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)124);
|
||||
}
|
||||
|
||||
void scan_vec3_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
float v[] = { 1.0f, 2.0f, 3.0f }; // 12 bytes
|
||||
// Last aligned 4-byte position where 12 bytes fit in [0, 128) is 116
|
||||
std::memcpy(data.data() + 116, v, 12);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Vec3, "1.0 2.0 3.0", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 4, 0, 128);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)116);
|
||||
}
|
||||
|
||||
void scan_pattern_atRegionStart() {
|
||||
QByteArray data(128, 0);
|
||||
data[20] = char(0x48); data[21] = char(0x8B); data[22] = char(0x05);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(parseSignature("48 8B 05", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 1, 20, 100);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)20);
|
||||
}
|
||||
|
||||
void scan_pattern_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
// 3-byte pattern, last position in [20, 100) is 97
|
||||
data[97] = char(0x48); data[98] = char(0x8B); data[99] = char(0x05);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(parseSignature("48 8B 05", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 1, 20, 100);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)97);
|
||||
}
|
||||
|
||||
void scan_pattern_withWildcard_atRegionEnd() {
|
||||
QByteArray data(128, 0);
|
||||
// "48 ?? 05" at last position 97 in [20, 100)
|
||||
data[97] = char(0x48); data[98] = char(0xFF); data[99] = char(0x05);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(parseSignature("48 ?? 05", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 1, 20, 100);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)97);
|
||||
}
|
||||
|
||||
void scan_int32_multiplePositions_inConstrainedRegion() {
|
||||
// Place int32 at first, middle, and last aligned positions in [32, 96).
|
||||
// Aligned positions: 32, 36, 40, ..., 88, 92. First=32, last=92, mid=60.
|
||||
QByteArray data(128, 0);
|
||||
int32_t v = 0xCAFEBABE;
|
||||
std::memcpy(data.data() + 32, &v, 4);
|
||||
std::memcpy(data.data() + 60, &v, 4);
|
||||
std::memcpy(data.data() + 92, &v, 4);
|
||||
// Also place one outside the constraint to verify it's excluded
|
||||
std::memcpy(data.data() + 8, &v, 4);
|
||||
std::memcpy(data.data() + 100, &v, 4);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::UInt32, "0xCAFEBABE", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 4, 32, 96);
|
||||
QCOMPARE(addrs.size(), 3);
|
||||
QCOMPARE(addrs[0], (uint64_t)32);
|
||||
QCOMPARE(addrs[1], (uint64_t)60);
|
||||
QCOMPARE(addrs[2], (uint64_t)92);
|
||||
}
|
||||
|
||||
void scan_pattern_multiplePositions_inConstrainedRegion() {
|
||||
// IDA-style pattern at first, last, and middle of [16, 80).
|
||||
// Pattern "AA BB" (2 bytes), alignment 1. First=16, last=78, mid=50.
|
||||
QByteArray data(128, 0);
|
||||
data[16] = char(0xAA); data[17] = char(0xBB);
|
||||
data[50] = char(0xAA); data[51] = char(0xBB);
|
||||
data[78] = char(0xAA); data[79] = char(0xBB);
|
||||
// Outside constraint
|
||||
data[10] = char(0xAA); data[11] = char(0xBB);
|
||||
data[90] = char(0xAA); data[91] = char(0xBB);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(parseSignature("AA BB", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 1, 16, 80);
|
||||
QCOMPARE(addrs.size(), 3);
|
||||
QCOMPARE(addrs[0], (uint64_t)16);
|
||||
QCOMPARE(addrs[1], (uint64_t)50);
|
||||
QCOMPARE(addrs[2], (uint64_t)78);
|
||||
}
|
||||
|
||||
|
||||
void scan_int8_alignment1_atRegionEnd() {
|
||||
// 1-byte value at last byte of constrained region [10, 50).
|
||||
QByteArray data(64, 0);
|
||||
data[49] = char(0x7F);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Int8, "127", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 1, 10, 50);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)49);
|
||||
}
|
||||
|
||||
void scan_uint16_alignment2_atRegionEnd() {
|
||||
// 2-byte value at last aligned-2 position in [10, 50) = offset 48.
|
||||
QByteArray data(64, 0);
|
||||
uint16_t v = 0xBEEF;
|
||||
std::memcpy(data.data() + 48, &v, 2);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::UInt16, "0xBEEF", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 2, 10, 50);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)48);
|
||||
}
|
||||
|
||||
void scan_alignment4_skipsUnaligned() {
|
||||
// int32 placed at unaligned offset 18 inside [16, 48). Alignment 4.
|
||||
// Aligned positions from 16: 16, 20, 24, 28, 32, 36, 40, 44.
|
||||
// Offset 18 is not aligned to 4 from the region start, so should be skipped.
|
||||
QByteArray data(64, 0);
|
||||
int32_t v = 0xDEADBEEF;
|
||||
std::memcpy(data.data() + 18, &v, 4); // unaligned
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::UInt32, "0xDEADBEEF", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 4, 16, 48);
|
||||
QCOMPARE(addrs.size(), 0);
|
||||
}
|
||||
|
||||
void scan_alignment8_skipsUnaligned() {
|
||||
// double placed at offset 12 inside [0, 64). Alignment 8.
|
||||
// Aligned positions: 0, 8, 16, 24, 32, 40, 48, 56.
|
||||
// Offset 12 is not 8-aligned, so should be skipped.
|
||||
QByteArray data(64, 0);
|
||||
double v = 99.99;
|
||||
std::memcpy(data.data() + 12, &v, 8); // unaligned
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::Double, "99.99", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 8, 0, 64);
|
||||
QCOMPARE(addrs.size(), 0);
|
||||
}
|
||||
|
||||
void scan_alignment2_findsAligned_skipsUnaligned() {
|
||||
// utf16 "Hi" (4 bytes) at aligned offset 20 and unaligned offset 33.
|
||||
// Constraint [16, 48), alignment 2. Should find only offset 20.
|
||||
QByteArray data(64, 0);
|
||||
uint16_t chars[] = { 'H', 'i' };
|
||||
std::memcpy(data.data() + 20, chars, 4); // aligned to 2
|
||||
std::memcpy(data.data() + 33, chars, 4); // unaligned to 2
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(serializeValue(ValueType::UTF16, "Hi", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 2, 16, 48);
|
||||
QCOMPARE(addrs.size(), 1);
|
||||
QCOMPARE(addrs[0], (uint64_t)20);
|
||||
}
|
||||
|
||||
void scan_alignment1_overlappingWrites() {
|
||||
// Pattern "AA BB" written at 20, then overwritten at 21, plus 25.
|
||||
// Second write clobbers offset 20's pattern; only 21 and 25 match.
|
||||
QByteArray data(48, 0);
|
||||
data[20] = char(0xAA); data[21] = char(0xBB);
|
||||
data[21] = char(0xAA); data[22] = char(0xBB); // overlapping at 21
|
||||
data[25] = char(0xAA); data[26] = char(0xBB);
|
||||
QByteArray pat, mask;
|
||||
QVERIFY(parseSignature("AA BB", pat, mask));
|
||||
auto addrs = scanConstrained(data, pat, mask, 1, 16, 32);
|
||||
QCOMPARE(addrs.size(), 2); // 21 and 25 (20 was overwritten)
|
||||
QCOMPARE(addrs[0], (uint64_t)21);
|
||||
QCOMPARE(addrs[1], (uint64_t)25);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestScanner)
|
||||
|
||||
Reference in New Issue
Block a user