mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: scanner panel with signature/value search, rescan, address delegate
- Signature mode (IDA-style patterns with wildcards) and value mode (typed exact match) - Async scan engine with progress, cancel support - Re-scan updates all results with unified progress (single-pass read + table build) - Previous value column appears after first re-scan - WinDbg backtick address format with dimmed leading zeros (AddressDelegate) - Inline editing: address expressions navigate, value edits write to provider - Right-click context menu: Copy Address, Copy Value, Go to Address - Auto-sized columns, themed buttons with icons, dynamic combo width - 49 UI tests covering scan, rescan, editing, theming, progress completion
This commit is contained in:
@@ -79,6 +79,10 @@ add_executable(Reclass
|
||||
src/imports/import_pdb.cpp
|
||||
src/imports/import_pdb_dialog.h
|
||||
src/imports/import_pdb_dialog.cpp
|
||||
src/scanner.h
|
||||
src/scanner.cpp
|
||||
src/scannerpanel.h
|
||||
src/scannerpanel.cpp
|
||||
src/mainwindow.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
@@ -235,6 +239,11 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_static_fields PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_static_fields COMMAND test_static_fields)
|
||||
|
||||
add_executable(test_scanner tests/test_scanner.cpp src/scanner.cpp)
|
||||
target_include_directories(test_scanner PRIVATE src)
|
||||
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner COMMAND test_scanner)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_import_pdb tests/test_import_pdb.cpp
|
||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
@@ -392,6 +401,14 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
|
||||
src/scanner.cpp src/scannerpanel.cpp src/addressparser.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_scanner_ui PRIVATE src)
|
||||
target_link_libraries(test_scanner_ui PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
|
||||
# Disabled: WinDbg provider test has build errors (lastError API changed)
|
||||
#if(WIN32)
|
||||
# add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
|
||||
@@ -57,5 +57,8 @@
|
||||
<file alias="remote.svg">vsicons/remote.svg</file>
|
||||
<file alias="plug.svg">vsicons/plug.svg</file>
|
||||
<file alias="clear-all.svg">vsicons/clear-all.svg</file>
|
||||
<file alias="search.svg">vsicons/search.svg</file>
|
||||
<file alias="regex.svg">vsicons/regex.svg</file>
|
||||
<file alias="refresh.svg">vsicons/refresh.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
507
src/scanner.cpp
Normal file
507
src/scanner.cpp
Normal file
@@ -0,0 +1,507 @@
|
||||
#include "scanner.h"
|
||||
#include <QtConcurrent>
|
||||
#include <QMetaObject>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── 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.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)
|
||||
{
|
||||
QVector<ScanResult> results;
|
||||
|
||||
if (!prov || req.pattern.isEmpty())
|
||||
return results;
|
||||
|
||||
auto regions = prov->enumerateRegions();
|
||||
|
||||
// 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 = req.pattern.size();
|
||||
const char* pat = req.pattern.constData();
|
||||
const char* msk = req.mask.constData();
|
||||
const int alignment = qMax(1, req.alignment);
|
||||
|
||||
// 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;
|
||||
totalBytes += r.size;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if ((uint64_t)patternLen > region.size) {
|
||||
scannedBytes += region.size;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int overlap = patternLen - 1;
|
||||
QByteArray chunk(qMin((uint64_t)kChunk, region.size), Qt::Uninitialized);
|
||||
|
||||
for (uint64_t off = 0; off < region.size; ) {
|
||||
if (m_abort.load()) break;
|
||||
|
||||
uint64_t remaining = region.size - off;
|
||||
int readLen = (int)qMin((uint64_t)chunk.size(), remaining);
|
||||
|
||||
if (!prov->read(region.base + off, chunk.data(), readLen)) {
|
||||
// Skip unreadable chunk
|
||||
off += readLen;
|
||||
scannedBytes += readLen;
|
||||
continue;
|
||||
}
|
||||
|
||||
int scanEnd = readLen - patternLen;
|
||||
const char* data = chunk.constData();
|
||||
|
||||
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 = region.base + off + (uint64_t)i;
|
||||
r.regionModule = region.moduleName;
|
||||
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:
|
||||
return results;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
85
src/scanner.h
Normal file
85
src/scanner.h
Normal file
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
#include "providers/provider.h"
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QFutureWatcher>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Scan request / result ──
|
||||
|
||||
struct ScanRequest {
|
||||
QByteArray pattern; // literal bytes to match
|
||||
QByteArray mask; // 0xFF = must match, 0x00 = wildcard
|
||||
|
||||
bool filterExecutable = false; // only scan +x regions
|
||||
bool filterWritable = false; // only scan +w regions
|
||||
|
||||
int alignment = 1; // 1 = every byte, 4 = dword, 8 = qword
|
||||
int maxResults = 50000;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
uint64_t address;
|
||||
QString regionModule;
|
||||
QByteArray scanValue; // cached bytes at scan/update time
|
||||
QByteArray previousValue; // value before last update
|
||||
};
|
||||
|
||||
// ── Value scan types ──
|
||||
|
||||
enum class ValueType {
|
||||
Int8, Int16, Int32, Int64,
|
||||
UInt8, UInt16, UInt32, UInt64,
|
||||
Float, Double,
|
||||
Vec2, Vec3, Vec4,
|
||||
UTF8, UTF16,
|
||||
HexBytes
|
||||
};
|
||||
|
||||
// ── Pattern parsing ──
|
||||
|
||||
// Parse IDA-style signature string ("48 8B ?? 05") into pattern + mask.
|
||||
// Returns true on success. On failure, sets errorMsg.
|
||||
bool parseSignature(const QString& input, QByteArray& pattern, QByteArray& mask,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Serialize a typed value into raw bytes for exact-match scanning.
|
||||
// Returns true on success. On failure, sets errorMsg.
|
||||
bool serializeValue(ValueType type, const QString& input,
|
||||
QByteArray& pattern, QByteArray& mask,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Natural alignment for a value type (used as default alignment for value scans).
|
||||
int naturalAlignment(ValueType type);
|
||||
|
||||
// ── Scan engine ──
|
||||
|
||||
class ScanEngine : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ScanEngine(QObject* parent = nullptr);
|
||||
|
||||
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
||||
void abort();
|
||||
bool isRunning() const;
|
||||
|
||||
signals:
|
||||
void progress(int percent);
|
||||
void finished(QVector<ScanResult> results);
|
||||
void error(QString message);
|
||||
|
||||
private:
|
||||
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
||||
|
||||
std::atomic<bool> m_abort{false};
|
||||
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
Q_DECLARE_METATYPE(QVector<rcx::ScanResult>)
|
||||
726
src/scannerpanel.cpp
Normal file
726
src/scannerpanel.cpp
Normal file
@@ -0,0 +1,726 @@
|
||||
#include "scannerpanel.h"
|
||||
#include "addressparser.h"
|
||||
#include <cstring>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
void AddressDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const {
|
||||
// Draw background (selection/hover handled by style)
|
||||
QStyleOptionViewItem opt = option;
|
||||
initStyleOption(&opt, index);
|
||||
opt.text.clear();
|
||||
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
|
||||
|
||||
QString text = index.data(Qt::DisplayRole).toString();
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
// Find first non-zero hex digit (skip backtick)
|
||||
int dimEnd = 0;
|
||||
for (int i = 0; i < text.size(); i++) {
|
||||
QChar c = text[i];
|
||||
if (c == '`') { dimEnd = i + 1; continue; }
|
||||
if (c != '0') break;
|
||||
dimEnd = i + 1;
|
||||
}
|
||||
|
||||
QRect textRect = opt.rect.adjusted(7, 0, -4, 0); // match item padding
|
||||
painter->setFont(opt.font);
|
||||
|
||||
if (dimEnd > 0) {
|
||||
painter->setPen(dimColor);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text.left(dimEnd));
|
||||
// Advance past dim prefix
|
||||
int dimWidth = painter->fontMetrics().horizontalAdvance(text.left(dimEnd));
|
||||
textRect.setLeft(textRect.left() + dimWidth);
|
||||
}
|
||||
painter->setPen(brightColor);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text.mid(dimEnd));
|
||||
}
|
||||
|
||||
ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_engine(new ScanEngine(this))
|
||||
{
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(6, 6, 6, 6);
|
||||
mainLayout->setSpacing(4);
|
||||
|
||||
// ── Row 1: Mode + pattern/value input ──
|
||||
auto* inputRow = new QHBoxLayout;
|
||||
inputRow->setSpacing(6);
|
||||
|
||||
m_modeCombo = new QComboBox(this);
|
||||
m_modeCombo->addItem(QIcon(QStringLiteral(":/vsicons/regex.svg")),
|
||||
QStringLiteral("Signature"));
|
||||
m_modeCombo->addItem(QIcon(QStringLiteral(":/vsicons/symbol-variable.svg")),
|
||||
QStringLiteral("Value"));
|
||||
updateComboWidth();
|
||||
inputRow->addWidget(m_modeCombo);
|
||||
|
||||
// Signature input
|
||||
m_patternLabel = new QLabel(QStringLiteral("Pattern:"), this);
|
||||
inputRow->addWidget(m_patternLabel);
|
||||
|
||||
m_patternEdit = new QLineEdit(this);
|
||||
m_patternEdit->setPlaceholderText(QStringLiteral("48 8B ?? 05 ?? ?? ?? ?? CC"));
|
||||
inputRow->addWidget(m_patternEdit, 1);
|
||||
|
||||
// Value input (hidden initially)
|
||||
m_typeLabel = new QLabel(QStringLiteral("Type:"), this);
|
||||
inputRow->addWidget(m_typeLabel);
|
||||
|
||||
m_typeCombo = new QComboBox(this);
|
||||
m_typeCombo->addItem(QStringLiteral("int8"), (int)ValueType::Int8);
|
||||
m_typeCombo->addItem(QStringLiteral("int16"), (int)ValueType::Int16);
|
||||
m_typeCombo->addItem(QStringLiteral("int32"), (int)ValueType::Int32);
|
||||
m_typeCombo->addItem(QStringLiteral("int64"), (int)ValueType::Int64);
|
||||
m_typeCombo->addItem(QStringLiteral("uint8"), (int)ValueType::UInt8);
|
||||
m_typeCombo->addItem(QStringLiteral("uint16"), (int)ValueType::UInt16);
|
||||
m_typeCombo->addItem(QStringLiteral("uint32"), (int)ValueType::UInt32);
|
||||
m_typeCombo->addItem(QStringLiteral("uint64"), (int)ValueType::UInt64);
|
||||
m_typeCombo->addItem(QStringLiteral("float"), (int)ValueType::Float);
|
||||
m_typeCombo->addItem(QStringLiteral("double"), (int)ValueType::Double);
|
||||
m_typeCombo->setCurrentIndex(2); // default: int32
|
||||
inputRow->addWidget(m_typeCombo);
|
||||
|
||||
m_valueLabel = new QLabel(QStringLiteral("Value:"), this);
|
||||
inputRow->addWidget(m_valueLabel);
|
||||
|
||||
m_valueEdit = new QLineEdit(this);
|
||||
m_valueEdit->setPlaceholderText(QStringLiteral("12345"));
|
||||
inputRow->addWidget(m_valueEdit, 1);
|
||||
|
||||
mainLayout->addLayout(inputRow);
|
||||
|
||||
// ── Row 2: Filters + scan button + progress ──
|
||||
auto* filterRow = new QHBoxLayout;
|
||||
filterRow->setSpacing(6);
|
||||
|
||||
m_execCheck = new QCheckBox(QStringLiteral("Executable"), this);
|
||||
filterRow->addWidget(m_execCheck);
|
||||
|
||||
m_writeCheck = new QCheckBox(QStringLiteral("Writable"), this);
|
||||
filterRow->addWidget(m_writeCheck);
|
||||
|
||||
filterRow->addStretch();
|
||||
|
||||
m_scanBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/search.svg")),
|
||||
QStringLiteral("Scan"), this);
|
||||
filterRow->addWidget(m_scanBtn);
|
||||
|
||||
m_updateBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/refresh.svg")),
|
||||
QStringLiteral("Re-scan"), this);
|
||||
m_updateBtn->setEnabled(false);
|
||||
filterRow->addWidget(m_updateBtn);
|
||||
|
||||
m_progressBar = new QProgressBar(this);
|
||||
m_progressBar->setRange(0, 100);
|
||||
m_progressBar->setTextVisible(true);
|
||||
m_progressBar->setFixedWidth(150);
|
||||
m_progressBar->hide();
|
||||
filterRow->addWidget(m_progressBar);
|
||||
|
||||
mainLayout->addLayout(filterRow);
|
||||
|
||||
// ── Results table ──
|
||||
m_resultTable = new QTableWidget(this);
|
||||
m_resultTable->setColumnCount(2);
|
||||
m_resultTable->horizontalHeader()->hide();
|
||||
m_resultTable->verticalHeader()->hide();
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(false);
|
||||
m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_resultTable->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_resultTable->setEditTriggers(QAbstractItemView::DoubleClicked);
|
||||
m_resultTable->setShowGrid(false);
|
||||
m_resultTable->setMouseTracking(true);
|
||||
m_resultTable->setFocusPolicy(Qt::StrongFocus);
|
||||
m_resultTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
// Address column delegate for dimmed leading zeros
|
||||
m_addrDelegate = new AddressDelegate(this);
|
||||
m_resultTable->setItemDelegateForColumn(0, m_addrDelegate);
|
||||
mainLayout->addWidget(m_resultTable, 1);
|
||||
|
||||
// ── Row 3: Status + action buttons ──
|
||||
auto* actionRow = new QHBoxLayout;
|
||||
actionRow->setSpacing(6);
|
||||
|
||||
m_statusLabel = new QLabel(QStringLiteral("Ready"), this);
|
||||
actionRow->addWidget(m_statusLabel, 1);
|
||||
|
||||
m_gotoBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/arrow-right.svg")),
|
||||
QStringLiteral("Go to Address"), this);
|
||||
m_gotoBtn->setEnabled(false);
|
||||
actionRow->addWidget(m_gotoBtn);
|
||||
|
||||
m_copyBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
|
||||
QStringLiteral("Copy Address"), this);
|
||||
m_copyBtn->setEnabled(false);
|
||||
actionRow->addWidget(m_copyBtn);
|
||||
|
||||
mainLayout->addLayout(actionRow);
|
||||
|
||||
// ── Initial visibility: signature mode ──
|
||||
m_typeLabel->hide();
|
||||
m_typeCombo->hide();
|
||||
m_valueLabel->hide();
|
||||
m_valueEdit->hide();
|
||||
|
||||
// ── Connections ──
|
||||
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ScannerPanel::onModeChanged);
|
||||
connect(m_scanBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onScanClicked);
|
||||
connect(m_updateBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onUpdateClicked);
|
||||
connect(m_gotoBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onGoToAddress);
|
||||
connect(m_copyBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onCopyAddress);
|
||||
connect(m_resultTable, &QTableWidget::cellDoubleClicked,
|
||||
this, &ScannerPanel::onResultDoubleClicked);
|
||||
connect(m_resultTable, &QTableWidget::cellChanged,
|
||||
this, &ScannerPanel::onCellEdited);
|
||||
connect(m_resultTable, &QTableWidget::itemSelectionChanged, this, [this]() {
|
||||
bool hasSel = !m_resultTable->selectedItems().isEmpty();
|
||||
m_gotoBtn->setEnabled(hasSel);
|
||||
m_copyBtn->setEnabled(hasSel);
|
||||
});
|
||||
|
||||
connect(m_resultTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
int row = m_resultTable->rowAt(pos.y());
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
QMenu menu;
|
||||
auto* copyAddr = menu.addAction(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
|
||||
QStringLiteral("Copy Address"));
|
||||
auto* copyVal = menu.addAction(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
|
||||
QStringLiteral("Copy Value"));
|
||||
auto* goTo = menu.addAction(QIcon(QStringLiteral(":/vsicons/arrow-right.svg")),
|
||||
QStringLiteral("Go to Address"));
|
||||
auto* chosen = menu.exec(m_resultTable->viewport()->mapToGlobal(pos));
|
||||
if (chosen == copyAddr) {
|
||||
QString addr = QStringLiteral("0x%1")
|
||||
.arg(m_results[row].address, 0, 16, QLatin1Char('0')).toUpper();
|
||||
QApplication::clipboard()->setText(addr);
|
||||
m_statusLabel->setText(QStringLiteral("Copied: %1").arg(addr));
|
||||
} else if (chosen == copyVal) {
|
||||
QApplication::clipboard()->setText(formatValue(m_results[row].scanValue));
|
||||
m_statusLabel->setText(QStringLiteral("Copied value"));
|
||||
} else if (chosen == goTo) {
|
||||
emit goToAddress(m_results[row].address);
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_engine, &ScanEngine::progress, this, [this](int pct) {
|
||||
m_progressBar->setValue(pct);
|
||||
});
|
||||
connect(m_engine, &ScanEngine::finished,
|
||||
this, &ScannerPanel::onScanFinished);
|
||||
connect(m_engine, &ScanEngine::error, this, [this](const QString& msg) {
|
||||
m_statusLabel->setText(QStringLiteral("Error: %1").arg(msg));
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
});
|
||||
}
|
||||
|
||||
void ScannerPanel::setProviderGetter(ProviderGetter getter) {
|
||||
m_providerGetter = std::move(getter);
|
||||
}
|
||||
|
||||
void ScannerPanel::setEditorFont(const QFont& font) {
|
||||
m_resultTable->setFont(font);
|
||||
QFontMetrics fm(font);
|
||||
m_resultTable->verticalHeader()->setDefaultSectionSize(fm.height() + 6);
|
||||
m_patternEdit->setFont(font);
|
||||
m_valueEdit->setFont(font);
|
||||
m_modeCombo->setFont(font);
|
||||
m_typeCombo->setFont(font);
|
||||
m_statusLabel->setFont(font);
|
||||
m_scanBtn->setFont(font);
|
||||
m_gotoBtn->setFont(font);
|
||||
m_copyBtn->setFont(font);
|
||||
m_patternLabel->setFont(font);
|
||||
m_typeLabel->setFont(font);
|
||||
m_valueLabel->setFont(font);
|
||||
m_execCheck->setFont(font);
|
||||
m_writeCheck->setFont(font);
|
||||
m_updateBtn->setFont(font);
|
||||
updateComboWidth();
|
||||
}
|
||||
|
||||
void ScannerPanel::updateComboWidth() {
|
||||
QFontMetrics fm(m_modeCombo->font());
|
||||
int maxW = 0;
|
||||
for (int i = 0; i < m_modeCombo->count(); i++)
|
||||
maxW = qMax(maxW, fm.horizontalAdvance(m_modeCombo->itemText(i)));
|
||||
m_modeCombo->setFixedWidth(maxW + 50); // icon + dropdown arrow + padding
|
||||
}
|
||||
|
||||
void ScannerPanel::onModeChanged(int index) {
|
||||
bool isSig = (index == 0);
|
||||
|
||||
m_patternLabel->setVisible(isSig);
|
||||
m_patternEdit->setVisible(isSig);
|
||||
|
||||
m_typeLabel->setVisible(!isSig);
|
||||
m_typeCombo->setVisible(!isSig);
|
||||
m_valueLabel->setVisible(!isSig);
|
||||
m_valueEdit->setVisible(!isSig);
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanClicked() {
|
||||
if (m_engine->isRunning()) {
|
||||
m_engine->abort();
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_statusLabel->setText(QStringLiteral("Scan cancelled"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get provider
|
||||
std::shared_ptr<Provider> provider;
|
||||
if (m_providerGetter)
|
||||
provider = m_providerGetter();
|
||||
|
||||
if (!provider) {
|
||||
m_statusLabel->setText(QStringLiteral("No source attached"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build request
|
||||
ScanRequest req = buildRequest();
|
||||
if (req.pattern.isEmpty())
|
||||
return; // error already shown by buildRequest
|
||||
|
||||
m_lastScanMode = m_modeCombo->currentIndex();
|
||||
if (m_lastScanMode == 1)
|
||||
m_lastValueType = (ValueType)m_typeCombo->currentData().toInt();
|
||||
m_lastPattern = req.pattern;
|
||||
|
||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
m_statusLabel->setText(QStringLiteral("Scanning..."));
|
||||
|
||||
m_engine->start(provider, req);
|
||||
}
|
||||
|
||||
ScanRequest ScannerPanel::buildRequest() {
|
||||
ScanRequest req;
|
||||
QString err;
|
||||
|
||||
if (m_modeCombo->currentIndex() == 0) {
|
||||
// Signature mode
|
||||
if (!parseSignature(m_patternEdit->text(), req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return {};
|
||||
}
|
||||
req.alignment = 1;
|
||||
} else {
|
||||
// Value mode
|
||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||
if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return {};
|
||||
}
|
||||
req.alignment = naturalAlignment(vt);
|
||||
}
|
||||
|
||||
req.filterExecutable = m_execCheck->isChecked();
|
||||
req.filterWritable = m_writeCheck->isChecked();
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_results = results;
|
||||
|
||||
// Cache scan-time bytes
|
||||
if (m_lastScanMode == 1) {
|
||||
// Value mode — every result matched the same value, no re-read needed
|
||||
for (auto& r : m_results) {
|
||||
r.previousValue.clear();
|
||||
r.scanValue = m_lastPattern;
|
||||
}
|
||||
} else {
|
||||
// Signature mode — wildcards mean each match may differ, read actual bytes
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
for (auto& r : m_results) {
|
||||
r.previousValue.clear();
|
||||
r.scanValue = prov ? prov->readBytes(r.address, 16) : QByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
m_updateBtn->setEnabled(!m_results.isEmpty());
|
||||
populateTable(false);
|
||||
|
||||
m_statusLabel->setText(QStringLiteral("%1 result%2")
|
||||
.arg(m_results.size())
|
||||
.arg(m_results.size() == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::populateTable(bool showPrevious) {
|
||||
m_resultTable->blockSignals(true);
|
||||
int cols = showPrevious ? 3 : 2;
|
||||
m_resultTable->setColumnCount(cols);
|
||||
m_resultTable->setRowCount(m_results.size());
|
||||
|
||||
for (int i = 0; i < m_results.size(); i++) {
|
||||
const auto& r = m_results[i];
|
||||
|
||||
// Address column — WinDbg backtick format: 00000000`00000000
|
||||
QString hexPart = QStringLiteral("%1").arg(r.address, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
auto* addrItem = new QTableWidgetItem(hexPart);
|
||||
addrItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 0, addrItem);
|
||||
|
||||
// Value column
|
||||
auto* valItem = new QTableWidgetItem(formatValue(r.scanValue));
|
||||
valItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 1, valItem);
|
||||
|
||||
// Previous column
|
||||
if (showPrevious) {
|
||||
auto* prevItem = new QTableWidgetItem(
|
||||
r.previousValue.isEmpty() ? QString() : formatValue(r.previousValue));
|
||||
prevItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
m_resultTable->setItem(i, 2, prevItem);
|
||||
}
|
||||
}
|
||||
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(false);
|
||||
m_resultTable->blockSignals(false);
|
||||
}
|
||||
|
||||
void ScannerPanel::onUpdateClicked() {
|
||||
if (m_results.isEmpty()) return;
|
||||
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
if (!prov) {
|
||||
m_statusLabel->setText(QStringLiteral("No source attached"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_updateBtn->setEnabled(false);
|
||||
m_scanBtn->setEnabled(false);
|
||||
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
int total = m_results.size();
|
||||
|
||||
// Single pass: read new values + build table rows
|
||||
m_resultTable->blockSignals(true);
|
||||
m_resultTable->setColumnCount(3);
|
||||
m_resultTable->setRowCount(total);
|
||||
|
||||
for (int i = 0; i < total; i++) {
|
||||
auto& r = m_results[i];
|
||||
r.previousValue = r.scanValue;
|
||||
r.scanValue = prov->readBytes(r.address, readSize);
|
||||
|
||||
QString hexPart = QStringLiteral("%1").arg(r.address, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
auto* addrItem = new QTableWidgetItem(hexPart);
|
||||
addrItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 0, addrItem);
|
||||
|
||||
auto* valItem = new QTableWidgetItem(formatValue(r.scanValue));
|
||||
valItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 1, valItem);
|
||||
|
||||
auto* prevItem = new QTableWidgetItem(formatValue(r.previousValue));
|
||||
prevItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
m_resultTable->setItem(i, 2, prevItem);
|
||||
|
||||
if ((i & 0xFF) == 0) {
|
||||
m_progressBar->setValue(i * 100 / total);
|
||||
QApplication::processEvents();
|
||||
}
|
||||
}
|
||||
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(false);
|
||||
m_resultTable->blockSignals(false);
|
||||
|
||||
m_progressBar->setValue(100);
|
||||
m_progressBar->hide();
|
||||
m_scanBtn->setEnabled(true);
|
||||
m_updateBtn->setEnabled(true);
|
||||
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
|
||||
.arg(total).arg(total == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::onGoToAddress() {
|
||||
int row = m_resultTable->currentRow();
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
emit goToAddress(m_results[row].address);
|
||||
}
|
||||
|
||||
void ScannerPanel::onCopyAddress() {
|
||||
int row = m_resultTable->currentRow();
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
|
||||
QString addr = QStringLiteral("0x%1")
|
||||
.arg(m_results[row].address, 0, 16, QLatin1Char('0')).toUpper();
|
||||
QApplication::clipboard()->setText(addr);
|
||||
m_statusLabel->setText(QStringLiteral("Copied: %1").arg(addr));
|
||||
}
|
||||
|
||||
void ScannerPanel::onResultDoubleClicked(int row, int col) {
|
||||
// Double-click on address column navigates (editing also starts via edit trigger)
|
||||
// Double-click on preview column only starts inline editing
|
||||
Q_UNUSED(col);
|
||||
Q_UNUSED(row);
|
||||
// Navigation is handled by Go to Address button or onCellEdited for address expressions
|
||||
}
|
||||
|
||||
void ScannerPanel::onCellEdited(int row, int col) {
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
|
||||
auto* item = m_resultTable->item(row, col);
|
||||
if (!item) return;
|
||||
QString text = item->text().trimmed();
|
||||
|
||||
if (col == 0) {
|
||||
// Address column — evaluate expression via AddressParser
|
||||
AddressParserCallbacks cbs;
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
if (prov) {
|
||||
auto* p = prov.get();
|
||||
cbs.resolveModule = [p](const QString& name, bool* ok) -> uint64_t {
|
||||
uint64_t base = p->symbolToAddress(name);
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
cbs.readPointer = [p](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = p->read(addr, &val, 8);
|
||||
return val;
|
||||
};
|
||||
}
|
||||
auto result = AddressParser::evaluate(text, 8, &cbs);
|
||||
if (result.ok) {
|
||||
m_results[row].address = result.value;
|
||||
emit goToAddress(result.value);
|
||||
// Reformat the address cell
|
||||
m_resultTable->blockSignals(true);
|
||||
QString hexPart = QStringLiteral("%1").arg(result.value, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
item->setText(hexPart);
|
||||
// Re-read preview at new address and update cache
|
||||
if (prov) {
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
m_results[row].scanValue = prov->readBytes(result.value, readSize);
|
||||
if (auto* prevItem = m_resultTable->item(row, 1))
|
||||
prevItem->setText(formatValue(m_results[row].scanValue));
|
||||
}
|
||||
m_resultTable->blockSignals(false);
|
||||
} else {
|
||||
m_statusLabel->setText(QStringLiteral("Expression error: %1").arg(result.error));
|
||||
// Restore original address
|
||||
m_resultTable->blockSignals(true);
|
||||
QString hexPart = QStringLiteral("%1").arg(m_results[row].address, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
item->setText(hexPart);
|
||||
m_resultTable->blockSignals(false);
|
||||
}
|
||||
} else if (col == 1) {
|
||||
// Preview column — parse hex bytes and write to provider
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
if (!prov || !prov->isWritable()) {
|
||||
m_statusLabel->setText(QStringLiteral("Provider is read-only"));
|
||||
return;
|
||||
}
|
||||
QByteArray bytes;
|
||||
uint64_t addr = m_results[row].address;
|
||||
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode — parse space-separated hex bytes
|
||||
QStringList tokens = text.split(' ', Qt::SkipEmptyParts);
|
||||
for (const QString& tok : tokens) {
|
||||
bool ok;
|
||||
uint val = tok.toUInt(&ok, 16);
|
||||
if (!ok || val > 0xFF) {
|
||||
m_statusLabel->setText(QStringLiteral("Invalid hex byte: %1").arg(tok));
|
||||
return;
|
||||
}
|
||||
bytes.append(char(val));
|
||||
}
|
||||
} else {
|
||||
// Value mode — parse native type
|
||||
bool ok = false;
|
||||
bytes.resize(valueSize());
|
||||
char* d = bytes.data();
|
||||
switch (m_lastValueType) {
|
||||
case ValueType::Int8: { auto v = (int8_t)text.toInt(&ok); if (ok) memcpy(d, &v, 1); break; }
|
||||
case ValueType::UInt8: { auto v = (uint8_t)text.toUInt(&ok); if (ok) memcpy(d, &v, 1); break; }
|
||||
case ValueType::Int16: { auto v = (int16_t)text.toShort(&ok); if (ok) memcpy(d, &v, 2); break; }
|
||||
case ValueType::UInt16: { auto v = text.toUShort(&ok); if (ok) memcpy(d, &v, 2); break; }
|
||||
case ValueType::Int32: { auto v = text.toInt(&ok); if (ok) memcpy(d, &v, 4); break; }
|
||||
case ValueType::UInt32: { auto v = text.toUInt(&ok); if (ok) memcpy(d, &v, 4); break; }
|
||||
case ValueType::Int64: { auto v = text.toLongLong(&ok); if (ok) memcpy(d, &v, 8); break; }
|
||||
case ValueType::UInt64: { auto v = text.toULongLong(&ok); if (ok) memcpy(d, &v, 8); break; }
|
||||
case ValueType::Float: { auto v = text.toFloat(&ok); if (ok) memcpy(d, &v, 4); break; }
|
||||
case ValueType::Double: { auto v = text.toDouble(&ok); if (ok) memcpy(d, &v, 8); break; }
|
||||
default: break;
|
||||
}
|
||||
if (!ok) {
|
||||
m_statusLabel->setText(QStringLiteral("Invalid value"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (bytes.isEmpty()) return;
|
||||
|
||||
if (prov->writeBytes(addr, bytes)) {
|
||||
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
|
||||
.arg(bytes.size())
|
||||
.arg(bytes.size() == 1 ? "" : "s")
|
||||
.arg(addr, 0, 16, QLatin1Char('0')).toUpper());
|
||||
// Re-read and update cache
|
||||
m_resultTable->blockSignals(true);
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
m_results[row].scanValue = prov->readBytes(addr, readSize);
|
||||
item->setText(formatValue(m_results[row].scanValue));
|
||||
m_resultTable->blockSignals(false);
|
||||
} else {
|
||||
m_statusLabel->setText(QStringLiteral("Write failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerPanel::applyTheme(const Theme& theme) {
|
||||
// Address delegate colors
|
||||
m_addrDelegate->dimColor = theme.textFaint;
|
||||
m_addrDelegate->brightColor = theme.text;
|
||||
|
||||
// Results table — editor-matching style
|
||||
m_resultTable->setStyleSheet(QStringLiteral(
|
||||
"QTableWidget { background: %1; color: %2; border: none; }"
|
||||
"QTableWidget::item { padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget QLineEdit { background: %1; color: %2; border: 1px solid %4;"
|
||||
" padding: 1px 4px; selection-background-color: %5; }")
|
||||
.arg(theme.background.name(), theme.text.name(), theme.hover.name(),
|
||||
theme.borderFocused.name(), theme.selection.name()));
|
||||
|
||||
// Input fields
|
||||
QString lineEditStyle = QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
|
||||
"QLineEdit:focus { border-color: %4; }")
|
||||
.arg(theme.background.name(), theme.text.name(),
|
||||
theme.border.name(), theme.borderFocused.name());
|
||||
m_patternEdit->setStyleSheet(lineEditStyle);
|
||||
m_valueEdit->setStyleSheet(lineEditStyle);
|
||||
|
||||
// Combo boxes
|
||||
QString comboStyle = QStringLiteral(
|
||||
"QComboBox { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px 2px 4px; }"
|
||||
"QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right;"
|
||||
" width: 16px; border-left: 1px solid %3; }"
|
||||
"QComboBox::down-arrow { image: url(:/vsicons/chevron-down.svg); width: 10px; height: 10px; }"
|
||||
"QComboBox QAbstractItemView { background: %1; color: %2; selection-background-color: %4; }")
|
||||
.arg(theme.background.name(), theme.text.name(),
|
||||
theme.border.name(), theme.hover.name());
|
||||
m_modeCombo->setStyleSheet(comboStyle);
|
||||
m_typeCombo->setStyleSheet(comboStyle);
|
||||
|
||||
// Labels
|
||||
QPalette lp;
|
||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_patternLabel->setPalette(lp);
|
||||
m_typeLabel->setPalette(lp);
|
||||
m_valueLabel->setPalette(lp);
|
||||
m_statusLabel->setPalette(lp);
|
||||
|
||||
// Checkboxes
|
||||
QPalette cp;
|
||||
cp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_execCheck->setPalette(cp);
|
||||
m_writeCheck->setPalette(cp);
|
||||
|
||||
// Buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
|
||||
"QPushButton:hover { background: %4; }"
|
||||
"QPushButton:pressed { background: %5; }"
|
||||
"QPushButton:disabled { color: %6; }")
|
||||
.arg(theme.button.name(), theme.text.name(), theme.border.name(),
|
||||
theme.hover.name(), theme.hover.darker(130).name(),
|
||||
theme.textMuted.name());
|
||||
m_scanBtn->setStyleSheet(btnStyle);
|
||||
m_updateBtn->setStyleSheet(btnStyle);
|
||||
m_gotoBtn->setStyleSheet(btnStyle);
|
||||
m_copyBtn->setStyleSheet(btnStyle);
|
||||
|
||||
// Progress bar
|
||||
m_progressBar->setStyleSheet(QStringLiteral(
|
||||
"QProgressBar { background: %1; border: 1px solid %2; text-align: center; color: %3; }"
|
||||
"QProgressBar::chunk { background: %4; }")
|
||||
.arg(theme.background.name(), theme.border.name(),
|
||||
theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
}
|
||||
|
||||
int ScannerPanel::valueSize() const {
|
||||
switch (m_lastValueType) {
|
||||
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;
|
||||
default: return 16;
|
||||
}
|
||||
}
|
||||
|
||||
QString ScannerPanel::formatValue(const QByteArray& bytes) const {
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode — hex bytes
|
||||
QString s;
|
||||
for (int j = 0; j < bytes.size(); j++) {
|
||||
if (j > 0) s += ' ';
|
||||
s += QStringLiteral("%1").arg((uint8_t)bytes[j], 2, 16, QLatin1Char('0')).toUpper();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// Value mode — native type
|
||||
const char* d = bytes.constData();
|
||||
int sz = bytes.size();
|
||||
switch (m_lastValueType) {
|
||||
case ValueType::Int8: if (sz >= 1) return QString::number((int8_t)d[0]); break;
|
||||
case ValueType::UInt8: if (sz >= 1) return QString::number((uint8_t)d[0]); break;
|
||||
case ValueType::Int16: if (sz >= 2) { int16_t v; memcpy(&v, d, 2); return QString::number(v); } break;
|
||||
case ValueType::UInt16: if (sz >= 2) { uint16_t v; memcpy(&v, d, 2); return QString::number(v); } break;
|
||||
case ValueType::Int32: if (sz >= 4) { int32_t v; memcpy(&v, d, 4); return QString::number(v); } break;
|
||||
case ValueType::UInt32: if (sz >= 4) { uint32_t v; memcpy(&v, d, 4); return QString::number(v); } break;
|
||||
case ValueType::Int64: if (sz >= 8) { int64_t v; memcpy(&v, d, 8); return QString::number(v); } break;
|
||||
case ValueType::UInt64: if (sz >= 8) { uint64_t v; memcpy(&v, d, 8); return QString::number(v); } break;
|
||||
case ValueType::Float: if (sz >= 4) { float v; memcpy(&v, d, 4); return QString::number(v, 'g', 9); } break;
|
||||
case ValueType::Double: if (sz >= 8) { double v; memcpy(&v, d, 8); return QString::number(v, 'g', 17); } break;
|
||||
default: break;
|
||||
}
|
||||
return QStringLiteral("??");
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
111
src/scannerpanel.h
Normal file
111
src/scannerpanel.h
Normal file
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
#include "scanner.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QWidget>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include <QProgressBar>
|
||||
#include <QTableWidget>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QLabel>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Delegate that paints address with dimmed high-bytes prefix
|
||||
class AddressDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
QColor dimColor;
|
||||
QColor brightColor;
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override;
|
||||
};
|
||||
|
||||
class ScannerPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ScannerPanel(QWidget* parent = nullptr);
|
||||
|
||||
using ProviderGetter = std::function<std::shared_ptr<Provider>()>;
|
||||
void setProviderGetter(ProviderGetter getter);
|
||||
|
||||
void setEditorFont(const QFont& font);
|
||||
void applyTheme(const Theme& theme);
|
||||
|
||||
// Test accessors
|
||||
QComboBox* modeCombo() const { return m_modeCombo; }
|
||||
QLineEdit* patternEdit() const { return m_patternEdit; }
|
||||
QComboBox* typeCombo() const { return m_typeCombo; }
|
||||
QLineEdit* valueEdit() const { return m_valueEdit; }
|
||||
QCheckBox* execCheck() const { return m_execCheck; }
|
||||
QCheckBox* writeCheck() const { return m_writeCheck; }
|
||||
QPushButton* scanButton() const { return m_scanBtn; }
|
||||
QPushButton* updateButton() const { return m_updateBtn; }
|
||||
QProgressBar* progressBar() const { return m_progressBar; }
|
||||
QTableWidget* resultsTable() const { return m_resultTable; }
|
||||
QLabel* statusLabel() const { return m_statusLabel; }
|
||||
QPushButton* gotoButton() const { return m_gotoBtn; }
|
||||
QPushButton* copyButton() const { return m_copyBtn; }
|
||||
ScanEngine* engine() const { return m_engine; }
|
||||
|
||||
signals:
|
||||
void goToAddress(uint64_t address);
|
||||
|
||||
private slots:
|
||||
void onModeChanged(int index);
|
||||
void onScanClicked();
|
||||
void onScanFinished(QVector<ScanResult> results);
|
||||
void onGoToAddress();
|
||||
void onCopyAddress();
|
||||
void onResultDoubleClicked(int row, int col);
|
||||
void onCellEdited(int row, int col);
|
||||
void onUpdateClicked();
|
||||
|
||||
private:
|
||||
ScanRequest buildRequest();
|
||||
void populateTable(bool showPrevious);
|
||||
void updateComboWidth();
|
||||
|
||||
// Input widgets
|
||||
QComboBox* m_modeCombo; // Signature / Value
|
||||
QLineEdit* m_patternEdit; // Signature pattern input
|
||||
QComboBox* m_typeCombo; // Value type dropdown
|
||||
QLineEdit* m_valueEdit; // Value input
|
||||
QLabel* m_patternLabel;
|
||||
QLabel* m_typeLabel;
|
||||
QLabel* m_valueLabel;
|
||||
|
||||
// Filters
|
||||
QCheckBox* m_execCheck;
|
||||
QCheckBox* m_writeCheck;
|
||||
|
||||
// Actions
|
||||
QPushButton* m_scanBtn;
|
||||
QPushButton* m_updateBtn;
|
||||
QProgressBar* m_progressBar;
|
||||
|
||||
// Results
|
||||
QTableWidget* m_resultTable;
|
||||
AddressDelegate* m_addrDelegate;
|
||||
QLabel* m_statusLabel;
|
||||
QPushButton* m_gotoBtn;
|
||||
QPushButton* m_copyBtn;
|
||||
|
||||
// Engine
|
||||
ScanEngine* m_engine;
|
||||
ProviderGetter m_providerGetter;
|
||||
QVector<ScanResult> m_results;
|
||||
int m_lastScanMode = 0; // 0=signature, 1=value
|
||||
ValueType m_lastValueType = ValueType::Int32;
|
||||
QByteArray m_lastPattern; // serialized search value
|
||||
|
||||
QString formatValue(const QByteArray& bytes) const;
|
||||
int valueSize() const;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
1078
tests/test_scanner.cpp
Normal file
1078
tests/test_scanner.cpp
Normal file
File diff suppressed because it is too large
Load Diff
960
tests/test_scanner_ui.cpp
Normal file
960
tests/test_scanner_ui.cpp
Normal file
@@ -0,0 +1,960 @@
|
||||
#include <QTest>
|
||||
#include <QSignalSpy>
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include <QProgressBar>
|
||||
#include <QTableWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QClipboard>
|
||||
#include <cstring>
|
||||
#include "scannerpanel.h"
|
||||
#include "scanner.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
#include "providers/null_provider.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestScannerUI : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
ScannerPanel* m_panel = nullptr;
|
||||
|
||||
private slots:
|
||||
|
||||
void init() {
|
||||
m_panel = new ScannerPanel();
|
||||
m_panel->show();
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
delete m_panel;
|
||||
m_panel = nullptr;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Widget creation and initial state
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void initialState_modeCombo() {
|
||||
QCOMPARE(m_panel->modeCombo()->count(), 2);
|
||||
QCOMPARE(m_panel->modeCombo()->currentIndex(), 0); // Signature
|
||||
QCOMPARE(m_panel->modeCombo()->itemText(0), QStringLiteral("Signature"));
|
||||
QCOMPARE(m_panel->modeCombo()->itemText(1), QStringLiteral("Value"));
|
||||
}
|
||||
|
||||
void initialState_signatureMode() {
|
||||
// In signature mode: pattern visible, value hidden
|
||||
QVERIFY(m_panel->patternEdit()->isVisible());
|
||||
QVERIFY(!m_panel->typeCombo()->isVisible());
|
||||
QVERIFY(!m_panel->valueEdit()->isVisible());
|
||||
}
|
||||
|
||||
void initialState_scanButton() {
|
||||
QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan"));
|
||||
}
|
||||
|
||||
void initialState_progressBarHidden() {
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
}
|
||||
|
||||
void initialState_resultsEmpty() {
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 0);
|
||||
}
|
||||
|
||||
void initialState_buttonsDisabled() {
|
||||
QVERIFY(!m_panel->gotoButton()->isEnabled());
|
||||
QVERIFY(!m_panel->copyButton()->isEnabled());
|
||||
}
|
||||
|
||||
void initialState_statusLabel() {
|
||||
QCOMPARE(m_panel->statusLabel()->text(), QStringLiteral("Ready"));
|
||||
}
|
||||
|
||||
void initialState_filterCheckboxes() {
|
||||
QVERIFY(!m_panel->execCheck()->isChecked());
|
||||
QVERIFY(!m_panel->writeCheck()->isChecked());
|
||||
}
|
||||
|
||||
void initialState_patternPlaceholder() {
|
||||
QVERIFY(!m_panel->patternEdit()->placeholderText().isEmpty());
|
||||
}
|
||||
|
||||
void initialState_typeComboHasAllTypes() {
|
||||
QCOMPARE(m_panel->typeCombo()->count(), 10); // int8..double = 10 types
|
||||
}
|
||||
|
||||
void initialState_resultsTableColumns() {
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 2);
|
||||
}
|
||||
|
||||
void initialState_noHeaders() {
|
||||
QVERIFY(!m_panel->resultsTable()->horizontalHeader()->isVisible());
|
||||
QVERIFY(!m_panel->resultsTable()->verticalHeader()->isVisible());
|
||||
}
|
||||
|
||||
void initialState_noGrid() {
|
||||
QVERIFY(!m_panel->resultsTable()->showGrid());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Mode switching
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void switchToValueMode() {
|
||||
m_panel->modeCombo()->setCurrentIndex(1); // Value
|
||||
|
||||
QVERIFY(!m_panel->patternEdit()->isVisible());
|
||||
QVERIFY(m_panel->typeCombo()->isVisible());
|
||||
QVERIFY(m_panel->valueEdit()->isVisible());
|
||||
}
|
||||
|
||||
void switchBackToSignatureMode() {
|
||||
m_panel->modeCombo()->setCurrentIndex(1); // Value
|
||||
m_panel->modeCombo()->setCurrentIndex(0); // Signature
|
||||
|
||||
QVERIFY(m_panel->patternEdit()->isVisible());
|
||||
QVERIFY(!m_panel->typeCombo()->isVisible());
|
||||
QVERIFY(!m_panel->valueEdit()->isVisible());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// No provider — error handling
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void scan_noProvider() {
|
||||
// No provider getter set
|
||||
m_panel->patternEdit()->setText("48 8B");
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("No source"));
|
||||
}
|
||||
|
||||
void scan_nullProvider() {
|
||||
m_panel->setProviderGetter([]() -> std::shared_ptr<Provider> {
|
||||
return nullptr;
|
||||
});
|
||||
m_panel->patternEdit()->setText("48 8B");
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("No source"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Invalid pattern — error handling
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void scan_emptyPattern() {
|
||||
auto prov = std::make_shared<BufferProvider>(QByteArray(16, '\0'));
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("");
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive));
|
||||
}
|
||||
|
||||
void scan_invalidPattern() {
|
||||
auto prov = std::make_shared<BufferProvider>(QByteArray(16, '\0'));
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("GG HH");
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Successful scan — signature mode
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void scan_signatureFindsResults() {
|
||||
QByteArray data(32, '\0');
|
||||
data[8] = 0x48; data[9] = 0x8B;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("48 8B");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("1 result"));
|
||||
}
|
||||
|
||||
void scan_signatureNoResults() {
|
||||
QByteArray data(32, '\0');
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("FF FF");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 0);
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("0 result"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Scan button toggle (Scan ↔ Cancel)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void scan_buttonShowsCancel() {
|
||||
// Use a large buffer so scan takes a measurable amount of time
|
||||
QByteArray data(4 * 1024 * 1024, '\0'); // 4MB
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("FF");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
// If scan hasn't finished yet, button should be Cancel
|
||||
if (m_panel->engine()->isRunning()) {
|
||||
QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Cancel"));
|
||||
QVERIFY(m_panel->progressBar()->isVisible());
|
||||
}
|
||||
|
||||
// Wait for finish
|
||||
QVERIFY(finSpy.wait(30000));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Button back to Scan
|
||||
QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan"));
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
}
|
||||
|
||||
void scan_cancelMidScan() {
|
||||
// This test verifies the cancel codepath works.
|
||||
// The scan may complete faster than we can click cancel,
|
||||
// so we just verify the final state is correct.
|
||||
QByteArray data(1024 * 1024, '\0'); // 1MB
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("FF");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
// Immediately try to cancel (may or may not succeed depending on timing)
|
||||
m_panel->engine()->abort();
|
||||
|
||||
// Wait for finished signal
|
||||
if (finSpy.isEmpty())
|
||||
QVERIFY(finSpy.wait(10000));
|
||||
QApplication::processEvents();
|
||||
|
||||
// After scan completes (or is cancelled), verify button returns to "Scan"
|
||||
// Need to process any remaining events (the finished handler sets button text)
|
||||
QApplication::processEvents();
|
||||
// If the panel still shows "Cancel", click it to reset
|
||||
if (m_panel->scanButton()->text() == QStringLiteral("Cancel")) {
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
}
|
||||
QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Value mode scan
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void scan_valueInt32() {
|
||||
QByteArray data(64, '\0');
|
||||
int32_t target = 42;
|
||||
std::memcpy(data.data() + 8, &target, 4);
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(1); // Value mode
|
||||
// Find int32 in combo
|
||||
for (int i = 0; i < m_panel->typeCombo()->count(); i++) {
|
||||
if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) {
|
||||
m_panel->typeCombo()->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_panel->valueEdit()->setText("42");
|
||||
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
// Preview should show native int32 value, not hex
|
||||
auto* prevItem = m_panel->resultsTable()->item(0, 1);
|
||||
QVERIFY(prevItem);
|
||||
QCOMPARE(prevItem->text(), QStringLiteral("42"));
|
||||
}
|
||||
|
||||
void scan_valueInvalidInput() {
|
||||
auto prov = std::make_shared<BufferProvider>(QByteArray(16, '\0'));
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(1); // Value
|
||||
m_panel->valueEdit()->setText("not_a_number");
|
||||
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Go to Address signal
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void goToAddress_signal() {
|
||||
QByteArray data(32, '\0');
|
||||
data[16] = 0xAB; data[17] = 0xCD;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("AB CD");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
|
||||
// Select the row
|
||||
m_panel->resultsTable()->selectRow(0);
|
||||
QVERIFY(m_panel->gotoButton()->isEnabled());
|
||||
|
||||
QSignalSpy goSpy(m_panel, &ScannerPanel::goToAddress);
|
||||
QTest::mouseClick(m_panel->gotoButton(), Qt::LeftButton);
|
||||
QCOMPARE(goSpy.size(), 1);
|
||||
QCOMPARE(goSpy.first().first().value<uint64_t>(), (uint64_t)16);
|
||||
}
|
||||
|
||||
void doubleClick_startsEditing() {
|
||||
// Double-click now starts inline editing, not goToAddress
|
||||
QByteArray data(32, '\0');
|
||||
data[4] = 0xEF;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("EF");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
|
||||
// Double-click should NOT emit goToAddress directly
|
||||
QSignalSpy goSpy(m_panel, &ScannerPanel::goToAddress);
|
||||
emit m_panel->resultsTable()->cellDoubleClicked(0, 0);
|
||||
QCOMPARE(goSpy.size(), 0);
|
||||
|
||||
// Edit triggers should be DoubleClicked
|
||||
QVERIFY(m_panel->resultsTable()->editTriggers() & QAbstractItemView::DoubleClicked);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Copy Address
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void copyAddress() {
|
||||
QByteArray data(32, '\0');
|
||||
data[20] = 0xAA;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("AA");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
m_panel->resultsTable()->selectRow(0);
|
||||
QVERIFY(m_panel->copyButton()->isEnabled());
|
||||
|
||||
QTest::mouseClick(m_panel->copyButton(), Qt::LeftButton);
|
||||
|
||||
QString clip = QApplication::clipboard()->text();
|
||||
QVERIFY(clip.startsWith("0x", Qt::CaseInsensitive));
|
||||
// 20 = 0x14, verify it's present somewhere
|
||||
QVERIFY(clip.toUpper().endsWith("14"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Results table formatting
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void results_addressFormat() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = (char)0xFF;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("FF");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Check address column has WinDbg backtick format
|
||||
auto* item = m_panel->resultsTable()->item(0, 0);
|
||||
QVERIFY(item);
|
||||
QVERIFY(item->text().contains('`')); // backtick separator
|
||||
// Address 0 should be: 00000000`00000000
|
||||
QCOMPARE(item->text(), QStringLiteral("00000000`00000000"));
|
||||
}
|
||||
|
||||
void results_previewColumn() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = 0xDE; data[1] = 0xAD; data[2] = 0xBE; data[3] = 0xEF;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("DE AD");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
auto* item = m_panel->resultsTable()->item(0, 1);
|
||||
QVERIFY(item);
|
||||
QVERIFY(item->text().contains("DE"));
|
||||
QVERIFY(item->text().contains("AD"));
|
||||
QVERIFY(item->text().contains("BE"));
|
||||
QVERIFY(item->text().contains("EF"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Filter checkboxes
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void filters_toggleExecutable() {
|
||||
m_panel->execCheck()->setChecked(true);
|
||||
QVERIFY(m_panel->execCheck()->isChecked());
|
||||
m_panel->execCheck()->setChecked(false);
|
||||
QVERIFY(!m_panel->execCheck()->isChecked());
|
||||
}
|
||||
|
||||
void filters_toggleWritable() {
|
||||
m_panel->writeCheck()->setChecked(true);
|
||||
QVERIFY(m_panel->writeCheck()->isChecked());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Theme application
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void theme_apply() {
|
||||
Theme t;
|
||||
t.background = QColor("#1e1e1e");
|
||||
t.backgroundAlt = QColor("#252526");
|
||||
t.text = QColor("#d4d4d4");
|
||||
t.textDim = QColor("#858585");
|
||||
t.textMuted = QColor("#555555");
|
||||
t.textFaint = QColor("#333333");
|
||||
t.border = QColor("#333333");
|
||||
t.borderFocused = QColor("#007acc");
|
||||
t.hover = QColor("#2a2d2e");
|
||||
t.selection = QColor("#264f78");
|
||||
t.button = QColor("#333333");
|
||||
t.indHoverSpan = QColor("#007acc");
|
||||
|
||||
// Should not crash
|
||||
m_panel->applyTheme(t);
|
||||
|
||||
// Verify stylesheet was applied (background color in stylesheet)
|
||||
QVERIFY(m_panel->resultsTable()->styleSheet().contains(t.background.name()));
|
||||
QVERIFY(m_panel->resultsTable()->styleSheet().contains(t.hover.name()));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Multiple scans — results get replaced
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void scan_resultsReplaced() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = 0xAA;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
// First scan
|
||||
m_panel->patternEdit()->setText("AA");
|
||||
QSignalSpy finSpy1(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy1.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
|
||||
// Second scan with different pattern (no results)
|
||||
m_panel->patternEdit()->setText("FF FF FF");
|
||||
QSignalSpy finSpy2(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy2.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Selection enables/disables action buttons
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void buttons_enableOnSelection() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = 0xBB; data[16] = 0xBB;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("BB");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 2);
|
||||
|
||||
// No selection yet
|
||||
QVERIFY(!m_panel->gotoButton()->isEnabled());
|
||||
QVERIFY(!m_panel->copyButton()->isEnabled());
|
||||
|
||||
// Select row
|
||||
m_panel->resultsTable()->selectRow(0);
|
||||
QVERIFY(m_panel->gotoButton()->isEnabled());
|
||||
QVERIFY(m_panel->copyButton()->isEnabled());
|
||||
|
||||
// Clear selection
|
||||
m_panel->resultsTable()->clearSelection();
|
||||
QVERIFY(!m_panel->gotoButton()->isEnabled());
|
||||
QVERIFY(!m_panel->copyButton()->isEnabled());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Wildcard signature scan
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void scan_wildcardSignature() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = 0x48; data[1] = 0x99; data[2] = 0x05;
|
||||
data[16] = 0x48; data[17] = 0xAA; data[18] = 0x05;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("48 ?? 05");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 2);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Multiple results — status label pluralization
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void status_singleResult() {
|
||||
QByteArray data(16, '\0');
|
||||
data[0] = 0xCC;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("CC");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->statusLabel()->text(), QStringLiteral("1 result"));
|
||||
}
|
||||
|
||||
void status_multipleResults() {
|
||||
QByteArray data(16, '\0');
|
||||
data[0] = 0xDD; data[8] = 0xDD;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("DD");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_panel->statusLabel()->text(), QStringLiteral("2 results"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Provider getter is lazy (captures at scan time)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Inline editing
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void edit_addressExpression() {
|
||||
QByteArray data(64, '\0');
|
||||
data[0] = 0xAA;
|
||||
data[32] = 0x55;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("AA");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
|
||||
// Edit address cell with hex expression
|
||||
QSignalSpy goSpy(m_panel, &ScannerPanel::goToAddress);
|
||||
m_panel->resultsTable()->item(0, 0)->setText("0x20");
|
||||
QApplication::processEvents();
|
||||
|
||||
// Should emit goToAddress with the new address
|
||||
QCOMPARE(goSpy.size(), 1);
|
||||
QCOMPARE(goSpy.first().first().value<uint64_t>(), (uint64_t)0x20);
|
||||
}
|
||||
|
||||
void edit_previewHex() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = 0xCC;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("CC");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
|
||||
// Edit preview cell with new hex bytes
|
||||
m_panel->resultsTable()->item(0, 1)->setText("DE AD BE EF");
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify bytes were written
|
||||
QByteArray written = prov->readBytes(0, 4);
|
||||
QCOMPARE((uint8_t)written[0], (uint8_t)0xDE);
|
||||
QCOMPARE((uint8_t)written[1], (uint8_t)0xAD);
|
||||
QCOMPARE((uint8_t)written[2], (uint8_t)0xBE);
|
||||
QCOMPARE((uint8_t)written[3], (uint8_t)0xEF);
|
||||
}
|
||||
|
||||
void edit_previewValueMode() {
|
||||
QByteArray data(64, '\0');
|
||||
int32_t target = 100;
|
||||
std::memcpy(data.data() + 8, &target, 4);
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(1); // Value mode
|
||||
for (int i = 0; i < m_panel->typeCombo()->count(); i++) {
|
||||
if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) {
|
||||
m_panel->typeCombo()->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_panel->valueEdit()->setText("100");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
|
||||
// Preview shows "100", edit to "999"
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("100"));
|
||||
m_panel->resultsTable()->item(0, 1)->setText("999");
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify int32 999 was written at offset 8
|
||||
int32_t written;
|
||||
QByteArray raw = prov->readBytes(8, 4);
|
||||
std::memcpy(&written, raw.constData(), 4);
|
||||
QCOMPARE(written, 999);
|
||||
}
|
||||
|
||||
void edit_invalidAddress() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = 0xDD;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("DD");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Edit with invalid expression — should show error and restore original
|
||||
m_panel->resultsTable()->item(0, 0)->setText("invalid!!!");
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive));
|
||||
// Address should be restored to original (00000000`00000000)
|
||||
QVERIFY(m_panel->resultsTable()->item(0, 0)->text().contains('`'));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Update button (rescan)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void update_disabledInitially() {
|
||||
QVERIFY(!m_panel->updateButton()->isEnabled());
|
||||
}
|
||||
|
||||
void update_enabledAfterScan() {
|
||||
QByteArray data(32, '\0');
|
||||
data[0] = 0xAA;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->patternEdit()->setText("AA");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(m_panel->updateButton()->isEnabled());
|
||||
}
|
||||
|
||||
void update_showsPreviousColumn() {
|
||||
QByteArray data(64, '\0');
|
||||
int32_t val = 50;
|
||||
std::memcpy(data.data() + 8, &val, 4);
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(1); // Value
|
||||
for (int i = 0; i < m_panel->typeCombo()->count(); i++) {
|
||||
if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) {
|
||||
m_panel->typeCombo()->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_panel->valueEdit()->setText("50");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 2); // no previous yet
|
||||
|
||||
// Modify via provider — change value from 50 to 99
|
||||
int32_t newVal = 99;
|
||||
QByteArray newBytes(4, '\0');
|
||||
std::memcpy(newBytes.data(), &newVal, 4);
|
||||
prov->writeBytes(8, newBytes);
|
||||
|
||||
// Click update
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 3);
|
||||
// Current value = 99, previous = 50
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("99"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("50"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Re-scan: progress completes, values update, table populates
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void update_progressCompletes() {
|
||||
// Use a buffer with many results to verify progress reaches 100%
|
||||
// and doesn't hang. Place int32 value "7" every 4 bytes.
|
||||
QByteArray data(4096, '\0');
|
||||
int32_t val = 7;
|
||||
for (int i = 0; i < 1024; i++)
|
||||
std::memcpy(data.data() + i * 4, &val, 4);
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(1); // Value
|
||||
for (int i = 0; i < m_panel->typeCombo()->count(); i++) {
|
||||
if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) {
|
||||
m_panel->typeCombo()->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_panel->valueEdit()->setText("7");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QVERIFY(m_panel->resultsTable()->rowCount() >= 512);
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 2);
|
||||
|
||||
// Modify all values: 7 → 21
|
||||
int32_t newVal = 21;
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
QByteArray nb(4, '\0');
|
||||
std::memcpy(nb.data(), &newVal, 4);
|
||||
prov->writeBytes(i * 4, nb);
|
||||
}
|
||||
|
||||
// Click Re-scan
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Progress bar should be hidden (completed)
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
// Table should have 3 columns now
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 3);
|
||||
// All rows should be populated
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), m_panel->resultsTable()->rowCount());
|
||||
// Spot check first and last row
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("21"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("7"));
|
||||
int lastRow = m_panel->resultsTable()->rowCount() - 1;
|
||||
QVERIFY(m_panel->resultsTable()->item(lastRow, 0) != nullptr);
|
||||
QCOMPARE(m_panel->resultsTable()->item(lastRow, 1)->text(), QStringLiteral("21"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(lastRow, 2)->text(), QStringLiteral("7"));
|
||||
// Status should say "Updated"
|
||||
QVERIFY(m_panel->statusLabel()->text().contains("Updated"));
|
||||
// Buttons should be re-enabled
|
||||
QVERIFY(m_panel->updateButton()->isEnabled());
|
||||
QVERIFY(m_panel->scanButton()->isEnabled());
|
||||
}
|
||||
|
||||
void update_signatureMode() {
|
||||
// Re-scan in signature mode (reads actual bytes, not cached pattern)
|
||||
QByteArray data(64, '\0');
|
||||
data[0] = 0x48; data[1] = 0x8B; data[2] = 0xAA;
|
||||
data[32] = 0x48; data[33] = 0x8B; data[34] = 0xBB;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(0); // Signature
|
||||
m_panel->patternEdit()->setText("48 8B ??");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 2);
|
||||
|
||||
// Modify bytes at first match
|
||||
QByteArray mod(3, '\0');
|
||||
mod[0] = 0x48; mod[1] = 0x8B; mod[2] = (char)0xFF;
|
||||
prov->writeBytes(0, mod);
|
||||
|
||||
// Re-scan
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 3);
|
||||
// First result current value should contain FF
|
||||
QVERIFY(m_panel->resultsTable()->item(0, 1)->text().contains("FF"));
|
||||
// First result previous value should contain AA
|
||||
QVERIFY(m_panel->resultsTable()->item(0, 2)->text().contains("AA"));
|
||||
}
|
||||
|
||||
void update_doubleRescan() {
|
||||
// Two consecutive re-scans: previous column updates each time
|
||||
QByteArray data(32, '\0');
|
||||
int32_t v1 = 10;
|
||||
std::memcpy(data.data() + 4, &v1, 4);
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(1);
|
||||
for (int i = 0; i < m_panel->typeCombo()->count(); i++) {
|
||||
if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) {
|
||||
m_panel->typeCombo()->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_panel->valueEdit()->setText("10");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), 1);
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("10"));
|
||||
|
||||
// First update: 10 → 20
|
||||
int32_t v2 = 20;
|
||||
QByteArray nb2(4, '\0');
|
||||
std::memcpy(nb2.data(), &v2, 4);
|
||||
prov->writeBytes(4, nb2);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("20"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("10"));
|
||||
|
||||
// Second update: 20 → 30
|
||||
int32_t v3 = 30;
|
||||
QByteArray nb3(4, '\0');
|
||||
std::memcpy(nb3.data(), &v3, 4);
|
||||
prov->writeBytes(4, nb3);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("30"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("20"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Provider getter is lazy (captures at scan time)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void providerGetter_lazy() {
|
||||
auto prov1 = std::make_shared<BufferProvider>(QByteArray(16, '\xAA'));
|
||||
auto prov2 = std::make_shared<BufferProvider>(QByteArray(16, '\xBB'));
|
||||
|
||||
auto current = prov1;
|
||||
m_panel->setProviderGetter([¤t]() { return current; });
|
||||
|
||||
// Scan with prov1 — should find AA
|
||||
m_panel->patternEdit()->setText("AA");
|
||||
QSignalSpy finSpy1(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy1.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QVERIFY(m_panel->resultsTable()->rowCount() > 0);
|
||||
|
||||
// Switch provider
|
||||
current = prov2;
|
||||
|
||||
// Scan for BB — should find in new provider
|
||||
m_panel->patternEdit()->setText("BB");
|
||||
QSignalSpy finSpy2(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy2.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QVERIFY(m_panel->resultsTable()->rowCount() > 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestScannerUI)
|
||||
#include "test_scanner_ui.moc"
|
||||
Reference in New Issue
Block a user