mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
782 lines
26 KiB
C++
782 lines
26 KiB
C++
#include "scanner.h"
|
|
#include <QtConcurrent>
|
|
#include <QMetaObject>
|
|
#include <QElapsedTimer>
|
|
#include <QDebug>
|
|
#include <cstring>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
|
|
namespace rcx {
|
|
|
|
// ── Pattern parsing ──
|
|
|
|
static int hexVal(QChar c) {
|
|
ushort u = c.unicode();
|
|
if (u >= '0' && u <= '9') return u - '0';
|
|
if (u >= 'a' && u <= 'f') return u - 'a' + 10;
|
|
if (u >= 'A' && u <= 'F') return u - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
bool parseSignature(const QString& input, QByteArray& pattern, QByteArray& mask,
|
|
QString* errorMsg)
|
|
{
|
|
pattern.clear();
|
|
mask.clear();
|
|
|
|
QString trimmed = input.trimmed();
|
|
if (trimmed.isEmpty()) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Empty pattern");
|
|
return false;
|
|
}
|
|
|
|
// Check for C-style: \xAB\xCD
|
|
if (trimmed.startsWith(QStringLiteral("\\x"))) {
|
|
QStringList parts = trimmed.split(QStringLiteral("\\x"), Qt::SkipEmptyParts);
|
|
for (const QString& part : parts) {
|
|
if (part.size() != 2) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid C-style byte: \\x%1").arg(part);
|
|
return false;
|
|
}
|
|
int hi = hexVal(part[0]);
|
|
int lo = hexVal(part[1]);
|
|
if (hi < 0 || lo < 0) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex char in: \\x%1").arg(part);
|
|
return false;
|
|
}
|
|
pattern.append(char((hi << 4) | lo));
|
|
mask.append(char(0xFF));
|
|
}
|
|
return !pattern.isEmpty();
|
|
}
|
|
|
|
// Space-separated or packed hex
|
|
bool hasSpaces = trimmed.contains(' ');
|
|
|
|
if (hasSpaces) {
|
|
QStringList tokens = trimmed.split(' ', Qt::SkipEmptyParts);
|
|
for (const QString& tok : tokens) {
|
|
if (tok == QStringLiteral("??") || tok == QStringLiteral("?")) {
|
|
pattern.append(char(0));
|
|
mask.append(char(0));
|
|
} else if (tok.size() == 2) {
|
|
int hi = hexVal(tok[0]);
|
|
int lo = hexVal(tok[1]);
|
|
if (hi < 0 || lo < 0) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex byte: %1").arg(tok);
|
|
return false;
|
|
}
|
|
pattern.append(char((hi << 4) | lo));
|
|
mask.append(char(0xFF));
|
|
} else {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid token: %1 (expected 2 hex chars or wildcards)").arg(tok);
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
// Packed: "488B??05"
|
|
if (trimmed.size() % 2 != 0) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Odd number of characters in packed pattern");
|
|
return false;
|
|
}
|
|
for (int i = 0; i < trimmed.size(); i += 2) {
|
|
QChar c0 = trimmed[i], c1 = trimmed[i + 1];
|
|
if ((c0 == '?' && c1 == '?')) {
|
|
pattern.append(char(0));
|
|
mask.append(char(0));
|
|
} else {
|
|
int hi = hexVal(c0);
|
|
int lo = hexVal(c1);
|
|
if (hi < 0 || lo < 0) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex chars at position %1: %2%3")
|
|
.arg(i).arg(c0).arg(c1);
|
|
return false;
|
|
}
|
|
pattern.append(char((hi << 4) | lo));
|
|
mask.append(char(0xFF));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pattern.isEmpty()) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Empty pattern after parsing");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ── Value serialization ──
|
|
|
|
template<typename T>
|
|
static void appendLE(QByteArray& out, T val) {
|
|
out.append(reinterpret_cast<const char*>(&val), sizeof(T));
|
|
}
|
|
|
|
bool serializeValue(ValueType type, const QString& input,
|
|
QByteArray& pattern, QByteArray& mask,
|
|
QString* errorMsg)
|
|
{
|
|
pattern.clear();
|
|
mask.clear();
|
|
|
|
QString trimmed = input.trimmed();
|
|
if (trimmed.isEmpty()) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Empty value");
|
|
return false;
|
|
}
|
|
|
|
bool ok = false;
|
|
|
|
switch (type) {
|
|
case ValueType::Int8: {
|
|
int v = trimmed.toInt(&ok);
|
|
if (!ok || v < -128 || v > 127) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid int8 value");
|
|
return false;
|
|
}
|
|
appendLE<int8_t>(pattern, (int8_t)v);
|
|
break;
|
|
}
|
|
case ValueType::Int16: {
|
|
int v = trimmed.toInt(&ok);
|
|
if (!ok || v < -32768 || v > 32767) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid int16 value");
|
|
return false;
|
|
}
|
|
appendLE<int16_t>(pattern, (int16_t)v);
|
|
break;
|
|
}
|
|
case ValueType::Int32: {
|
|
int v = trimmed.toInt(&ok);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid int32 value");
|
|
return false;
|
|
}
|
|
appendLE<int32_t>(pattern, (int32_t)v);
|
|
break;
|
|
}
|
|
case ValueType::Int64: {
|
|
qlonglong v = trimmed.toLongLong(&ok);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid int64 value");
|
|
return false;
|
|
}
|
|
appendLE<int64_t>(pattern, (int64_t)v);
|
|
break;
|
|
}
|
|
case ValueType::UInt8: {
|
|
uint v = trimmed.toUInt(&ok);
|
|
if (!ok || v > 255) {
|
|
// Try hex
|
|
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
|
v = trimmed.toUInt(&ok, 16);
|
|
if (!ok || v > 255) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint8 value");
|
|
return false;
|
|
}
|
|
}
|
|
appendLE<uint8_t>(pattern, (uint8_t)v);
|
|
break;
|
|
}
|
|
case ValueType::UInt16: {
|
|
uint v = trimmed.toUInt(&ok);
|
|
if (!ok || v > 65535) {
|
|
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
|
v = trimmed.toUInt(&ok, 16);
|
|
if (!ok || v > 65535) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint16 value");
|
|
return false;
|
|
}
|
|
}
|
|
appendLE<uint16_t>(pattern, (uint16_t)v);
|
|
break;
|
|
}
|
|
case ValueType::UInt32: {
|
|
quint32 v = trimmed.toULong(&ok);
|
|
if (!ok) {
|
|
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
|
v = trimmed.toULong(&ok, 16);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint32 value");
|
|
return false;
|
|
}
|
|
}
|
|
appendLE<uint32_t>(pattern, v);
|
|
break;
|
|
}
|
|
case ValueType::UInt64: {
|
|
quint64 v = trimmed.toULongLong(&ok);
|
|
if (!ok) {
|
|
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
|
v = trimmed.toULongLong(&ok, 16);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint64 value");
|
|
return false;
|
|
}
|
|
}
|
|
appendLE<uint64_t>(pattern, v);
|
|
break;
|
|
}
|
|
case ValueType::Float: {
|
|
float v = trimmed.toFloat(&ok);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid float value");
|
|
return false;
|
|
}
|
|
appendLE<float>(pattern, v);
|
|
break;
|
|
}
|
|
case ValueType::Double: {
|
|
double v = trimmed.toDouble(&ok);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid double value");
|
|
return false;
|
|
}
|
|
appendLE<double>(pattern, v);
|
|
break;
|
|
}
|
|
case ValueType::Vec2: {
|
|
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
|
if (parts.size() != 2) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Vec2 requires 2 space-separated floats");
|
|
return false;
|
|
}
|
|
for (const QString& p : parts) {
|
|
float v = p.toFloat(&ok);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec2: %1").arg(p);
|
|
return false;
|
|
}
|
|
appendLE<float>(pattern, v);
|
|
}
|
|
break;
|
|
}
|
|
case ValueType::Vec3: {
|
|
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
|
if (parts.size() != 3) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Vec3 requires 3 space-separated floats");
|
|
return false;
|
|
}
|
|
for (const QString& p : parts) {
|
|
float v = p.toFloat(&ok);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec3: %1").arg(p);
|
|
return false;
|
|
}
|
|
appendLE<float>(pattern, v);
|
|
}
|
|
break;
|
|
}
|
|
case ValueType::Vec4: {
|
|
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
|
if (parts.size() != 4) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Vec4 requires 4 space-separated floats");
|
|
return false;
|
|
}
|
|
for (const QString& p : parts) {
|
|
float v = p.toFloat(&ok);
|
|
if (!ok) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec4: %1").arg(p);
|
|
return false;
|
|
}
|
|
appendLE<float>(pattern, v);
|
|
}
|
|
break;
|
|
}
|
|
case ValueType::UTF8: {
|
|
QByteArray encoded = trimmed.toUtf8();
|
|
if (encoded.isEmpty()) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-8 string");
|
|
return false;
|
|
}
|
|
pattern = encoded;
|
|
break;
|
|
}
|
|
case ValueType::UTF16: {
|
|
// UTF-16LE encoding
|
|
for (int i = 0; i < trimmed.size(); i++) {
|
|
ushort u = trimmed[i].unicode();
|
|
appendLE<uint16_t>(pattern, u);
|
|
}
|
|
if (pattern.isEmpty()) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-16 string");
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
case ValueType::HexBytes: {
|
|
// Parse hex bytes (like signature but no wildcards)
|
|
QByteArray dummyMask;
|
|
if (!parseSignature(trimmed, pattern, dummyMask, errorMsg))
|
|
return false;
|
|
// HexBytes = exact match, no wildcards
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set mask to all 0xFF (exact match) for value scans
|
|
mask.fill(char(0xFF), pattern.size());
|
|
return true;
|
|
}
|
|
|
|
int naturalAlignment(ValueType type) {
|
|
switch (type) {
|
|
case ValueType::Int8:
|
|
case ValueType::UInt8:
|
|
case ValueType::UTF8:
|
|
case ValueType::HexBytes:
|
|
return 1;
|
|
case ValueType::Int16:
|
|
case ValueType::UInt16:
|
|
case ValueType::UTF16:
|
|
return 2;
|
|
case ValueType::Int32:
|
|
case ValueType::UInt32:
|
|
case ValueType::Float:
|
|
case ValueType::Vec2:
|
|
case ValueType::Vec3:
|
|
case ValueType::Vec4:
|
|
return 4;
|
|
case ValueType::Int64:
|
|
case ValueType::UInt64:
|
|
case ValueType::Double:
|
|
return 8;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int valueSizeForType(ValueType type) {
|
|
switch (type) {
|
|
case ValueType::Int8: case ValueType::UInt8: return 1;
|
|
case ValueType::Int16: case ValueType::UInt16: return 2;
|
|
case ValueType::Int32: case ValueType::UInt32: case ValueType::Float: return 4;
|
|
case ValueType::Int64: case ValueType::UInt64: case ValueType::Double: return 8;
|
|
case ValueType::Vec2: return 8;
|
|
case ValueType::Vec3: return 12;
|
|
case ValueType::Vec4: return 16;
|
|
default: return 4;
|
|
}
|
|
}
|
|
|
|
// ── Typed comparison for rescan conditions ──
|
|
|
|
static int compareTyped(const QByteArray& a, const QByteArray& b, ValueType vt) {
|
|
const char* da = a.constData();
|
|
const char* db = b.constData();
|
|
int sz = qMin(a.size(), b.size());
|
|
|
|
switch (vt) {
|
|
case ValueType::Int8:
|
|
if (sz >= 1) { int8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::UInt8:
|
|
if (sz >= 1) { uint8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::Int16:
|
|
if (sz >= 2) { int16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::UInt16:
|
|
if (sz >= 2) { uint16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::Int32:
|
|
if (sz >= 4) { int32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::UInt32:
|
|
if (sz >= 4) { uint32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::Int64:
|
|
if (sz >= 8) { int64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::UInt64:
|
|
if (sz >= 8) { uint64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::Float:
|
|
if (sz >= 4) { float va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
|
|
break;
|
|
case ValueType::Double:
|
|
if (sz >= 8) { double va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// Fallback: byte comparison
|
|
return memcmp(da, db, sz);
|
|
}
|
|
|
|
// ── Scan engine ──
|
|
|
|
ScanEngine::ScanEngine(QObject* parent)
|
|
: QObject(parent)
|
|
{
|
|
qRegisterMetaType<QVector<ScanResult>>("QVector<rcx::ScanResult>");
|
|
}
|
|
|
|
bool ScanEngine::isRunning() const {
|
|
return m_watcher && m_watcher->isRunning();
|
|
}
|
|
|
|
void ScanEngine::abort() {
|
|
m_abort.store(true);
|
|
}
|
|
|
|
void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& req) {
|
|
if (isRunning()) return;
|
|
|
|
if (req.condition != ScanCondition::UnknownValue) {
|
|
if (req.pattern.isEmpty()) {
|
|
emit error(QStringLiteral("Empty pattern"));
|
|
return;
|
|
}
|
|
if (req.pattern.size() != req.mask.size()) {
|
|
emit error(QStringLiteral("Pattern and mask size mismatch"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_abort.store(false);
|
|
|
|
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
|
|
m_watcher = watcher;
|
|
|
|
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
|
|
auto results = watcher->result();
|
|
watcher->deleteLater();
|
|
if (m_watcher == watcher)
|
|
m_watcher = nullptr;
|
|
emit finished(results);
|
|
});
|
|
|
|
watcher->setFuture(QtConcurrent::run([this, provider, req]() {
|
|
return runScan(provider, req);
|
|
}));
|
|
}
|
|
|
|
QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
|
const ScanRequest& req)
|
|
{
|
|
QElapsedTimer timer;
|
|
timer.start();
|
|
|
|
QVector<ScanResult> results;
|
|
const bool isUnknown = (req.condition == ScanCondition::UnknownValue);
|
|
|
|
if (!prov || (!isUnknown && req.pattern.isEmpty()))
|
|
return results;
|
|
|
|
auto regions = prov->enumerateRegions();
|
|
qDebug() << "[scan] regions:" << regions.size()
|
|
<< " pattern:" << req.pattern.size() << "bytes"
|
|
<< " align:" << req.alignment
|
|
<< " condition:" << (int)req.condition
|
|
<< " filterExec:" << req.filterExecutable
|
|
<< " filterWrite:" << req.filterWritable;
|
|
|
|
// Fallback for providers that don't enumerate regions (file/buffer)
|
|
if (regions.isEmpty()) {
|
|
MemoryRegion fallback;
|
|
fallback.base = 0;
|
|
fallback.size = (uint64_t)prov->size();
|
|
fallback.readable = true;
|
|
fallback.writable = true;
|
|
fallback.executable = false;
|
|
regions.append(fallback);
|
|
}
|
|
|
|
const int patternLen = isUnknown ? req.valueSize : req.pattern.size();
|
|
const char* pat = isUnknown ? nullptr : req.pattern.constData();
|
|
const char* msk = isUnknown ? nullptr : req.mask.constData();
|
|
const int alignment = qMax(1, req.alignment);
|
|
const int valSize = isUnknown ? req.valueSize : patternLen;
|
|
const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) &&
|
|
req.endAddress > req.startAddress;
|
|
|
|
// Pre-compute total bytes for progress
|
|
uint64_t totalBytes = 0;
|
|
for (const auto& r : regions) {
|
|
if (req.filterExecutable && !r.executable) continue;
|
|
if (req.filterWritable && !r.writable) continue;
|
|
uint64_t rStart = r.base, rEnd = r.base + r.size;
|
|
if (hasRange) {
|
|
if (rEnd <= req.startAddress || rStart >= req.endAddress) continue;
|
|
rStart = qMax(rStart, req.startAddress);
|
|
rEnd = qMin(rEnd, req.endAddress);
|
|
}
|
|
totalBytes += rEnd - rStart;
|
|
}
|
|
|
|
qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";
|
|
|
|
if (totalBytes == 0) return results;
|
|
|
|
uint64_t scannedBytes = 0;
|
|
int lastPct = -1;
|
|
|
|
constexpr int kChunk = 256 * 1024;
|
|
|
|
for (const auto& region : regions) {
|
|
if (m_abort.load()) break;
|
|
|
|
if (req.filterExecutable && !region.executable) continue;
|
|
if (req.filterWritable && !region.writable) continue;
|
|
|
|
// Clip region to requested address range
|
|
uint64_t regStart = region.base;
|
|
uint64_t regEnd = region.base + region.size;
|
|
if (hasRange) {
|
|
if (regEnd <= req.startAddress || regStart >= req.endAddress) {
|
|
// Entirely outside range — skip
|
|
continue;
|
|
}
|
|
regStart = qMax(regStart, req.startAddress);
|
|
regEnd = qMin(regEnd, req.endAddress);
|
|
}
|
|
uint64_t regSize = regEnd - regStart;
|
|
if (regSize == 0) continue;
|
|
|
|
if ((uint64_t)patternLen > regSize) {
|
|
scannedBytes += regSize;
|
|
continue;
|
|
}
|
|
|
|
const int overlap = patternLen - 1;
|
|
QByteArray chunk(qMin((uint64_t)kChunk, regSize), Qt::Uninitialized);
|
|
uint64_t regOffset = regStart - region.base; // offset within provider region
|
|
|
|
for (uint64_t off = 0; off < regSize; ) {
|
|
if (m_abort.load()) break;
|
|
|
|
uint64_t remaining = regSize - off;
|
|
int readLen = (int)qMin((uint64_t)chunk.size(), remaining);
|
|
|
|
if (!prov->read(regStart + off, chunk.data(), readLen)) {
|
|
// Skip unreadable chunk
|
|
off += readLen;
|
|
scannedBytes += readLen;
|
|
continue;
|
|
}
|
|
|
|
int scanEnd = readLen - patternLen;
|
|
const char* data = chunk.constData();
|
|
|
|
if (isUnknown) {
|
|
// Unknown value: capture every aligned address
|
|
for (int i = 0; i <= scanEnd; i += alignment) {
|
|
ScanResult r;
|
|
r.address = regStart + off + (uint64_t)i;
|
|
r.scanValue = QByteArray(data + i, valSize);
|
|
results.append(r);
|
|
|
|
if (results.size() >= req.maxResults)
|
|
goto done;
|
|
}
|
|
} else {
|
|
// Exact pattern match
|
|
for (int i = 0; i <= scanEnd; i += alignment) {
|
|
bool match = true;
|
|
for (int j = 0; j < patternLen; j++) {
|
|
if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) {
|
|
ScanResult r;
|
|
r.address = regStart + off + (uint64_t)i;
|
|
r.regionModule = region.moduleName;
|
|
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
|
|
results.append(r);
|
|
|
|
if (results.size() >= req.maxResults)
|
|
goto done;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Advance with overlap to catch patterns that straddle chunks
|
|
uint64_t advance;
|
|
if (readLen > overlap)
|
|
advance = (uint64_t)(readLen - overlap);
|
|
else
|
|
advance = 1; // prevent infinite loop on tiny regions
|
|
scannedBytes += advance;
|
|
off += advance;
|
|
|
|
// Throttled progress
|
|
int pct = (int)(scannedBytes * 100 / totalBytes);
|
|
if (pct > 100) pct = 100;
|
|
if (pct != lastPct) {
|
|
lastPct = pct;
|
|
QMetaObject::invokeMethod(this, "progress",
|
|
Qt::QueuedConnection, Q_ARG(int, pct));
|
|
}
|
|
}
|
|
}
|
|
|
|
done:
|
|
qDebug() << "[scan] done:" << results.size() << "results in" << timer.elapsed() << "ms"
|
|
<< " scanned:" << (scannedBytes / 1024) << "KB";
|
|
return results;
|
|
}
|
|
|
|
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
|
QVector<ScanResult> results, int readSize,
|
|
ScanCondition condition, ValueType valueType,
|
|
const QByteArray& filterPattern,
|
|
const QByteArray& filterMask) {
|
|
if (isRunning()) return;
|
|
|
|
m_abort.store(false);
|
|
|
|
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
|
|
m_watcher = watcher;
|
|
|
|
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
|
|
auto results = watcher->result();
|
|
watcher->deleteLater();
|
|
if (m_watcher == watcher)
|
|
m_watcher = nullptr;
|
|
emit rescanFinished(results);
|
|
});
|
|
|
|
watcher->setFuture(QtConcurrent::run(
|
|
[this, provider, results = std::move(results), readSize,
|
|
condition, valueType, filterPattern, filterMask]() mutable {
|
|
return runRescan(provider, std::move(results), readSize,
|
|
condition, valueType, filterPattern, filterMask);
|
|
}));
|
|
}
|
|
|
|
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
|
QVector<ScanResult> results, int readSize,
|
|
ScanCondition condition, ValueType valueType,
|
|
const QByteArray& filterPattern,
|
|
const QByteArray& filterMask) {
|
|
QElapsedTimer timer;
|
|
timer.start();
|
|
|
|
int total = results.size();
|
|
if (total == 0 || !prov) return results;
|
|
|
|
bool hasExactFilter = !filterPattern.isEmpty() && condition == ScanCondition::ExactValue;
|
|
bool hasComparison = (condition == ScanCondition::Changed ||
|
|
condition == ScanCondition::Unchanged ||
|
|
condition == ScanCondition::Increased ||
|
|
condition == ScanCondition::Decreased);
|
|
bool needsFilter = hasExactFilter || hasComparison;
|
|
|
|
qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize
|
|
<< "condition:" << (int)condition
|
|
<< "exactFilter:" << (hasExactFilter ? "yes" : "no")
|
|
<< "comparison:" << (hasComparison ? "yes" : "no");
|
|
|
|
// Save previous values
|
|
for (auto& r : results)
|
|
r.previousValue = r.scanValue;
|
|
|
|
// Sort indices by address for sequential chunked reads
|
|
QVector<int> order(total);
|
|
for (int i = 0; i < total; i++) order[i] = i;
|
|
std::sort(order.begin(), order.end(), [&results](int a, int b) {
|
|
return results[a].address < results[b].address;
|
|
});
|
|
|
|
constexpr int kChunk = 256 * 1024;
|
|
int updated = 0;
|
|
int lastPct = -1;
|
|
int chunks = 0;
|
|
uint64_t totalBytesRead = 0;
|
|
int i = 0;
|
|
|
|
// Track which results matched (by original index)
|
|
QVector<bool> matched(total, !needsFilter); // if no filter, all match
|
|
|
|
while (i < total && !m_abort.load()) {
|
|
uint64_t spanBase = results[order[i]].address;
|
|
int spanEnd = i;
|
|
|
|
// Extend span while next result fits in the same chunk
|
|
while (spanEnd + 1 < total) {
|
|
uint64_t endAddr = results[order[spanEnd + 1]].address + readSize;
|
|
if (endAddr - spanBase > (uint64_t)kChunk) break;
|
|
spanEnd++;
|
|
}
|
|
|
|
uint64_t spanLast = results[order[spanEnd]].address;
|
|
int chunkLen = (int)(spanLast + readSize - spanBase);
|
|
QByteArray chunk(chunkLen, '\0');
|
|
prov->read(spanBase, chunk.data(), chunkLen);
|
|
|
|
for (int j = i; j <= spanEnd; j++) {
|
|
int idx = order[j];
|
|
auto& r = results[idx];
|
|
int off = (int)(r.address - spanBase);
|
|
r.scanValue = chunk.mid(off, readSize);
|
|
|
|
// Apply exact-value filter
|
|
if (hasExactFilter) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Apply comparison-based filter
|
|
if (hasComparison && !r.previousValue.isEmpty()) {
|
|
int cmp = compareTyped(r.scanValue, r.previousValue, valueType);
|
|
switch (condition) {
|
|
case ScanCondition::Changed: matched[idx] = (cmp != 0); break;
|
|
case ScanCondition::Unchanged: matched[idx] = (cmp == 0); break;
|
|
case ScanCondition::Increased: matched[idx] = (cmp > 0); break;
|
|
case ScanCondition::Decreased: matched[idx] = (cmp < 0); break;
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
|
|
chunks++;
|
|
totalBytesRead += chunkLen;
|
|
updated += (spanEnd - i + 1);
|
|
i = spanEnd + 1;
|
|
|
|
int pct = updated * 100 / total;
|
|
if (pct != lastPct) {
|
|
lastPct = pct;
|
|
QMetaObject::invokeMethod(this, "progress",
|
|
Qt::QueuedConnection, Q_ARG(int, pct));
|
|
}
|
|
}
|
|
|
|
// Filter out non-matching results
|
|
if (needsFilter) {
|
|
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";
|
|
return results;
|
|
}
|
|
|
|
} // namespace rcx
|