diff --git a/CMakeLists.txt b/CMakeLists.txt index 37189e0..0a2bb5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -409,15 +409,15 @@ if(BUILD_TESTING) ${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 - # plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp) - # target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory) - # target_link_libraries(test_windbg_provider PRIVATE - # ${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32) - # add_test(NAME test_windbg_provider COMMAND test_windbg_provider) - #endif() + if(WIN32) + add_executable(test_windbg_provider tests/test_windbg_provider.cpp + plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp + src/scanner.cpp) + target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory) + target_link_libraries(test_windbg_provider PRIVATE + ${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32) + add_test(NAME test_windbg_provider COMMAND test_windbg_provider) + endif() add_executable(bench_large_class tests/bench_large_class.cpp src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp index 2a14a90..522b525 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp @@ -124,6 +124,51 @@ void ProcessMemoryProvider::cacheModules() } } +QVector ProcessMemoryProvider::enumerateRegions() const +{ + QVector regions; + if (!m_handle) return regions; + + MEMORY_BASIC_INFORMATION mbi; + uint64_t addr = 0; + + while (VirtualQueryEx(m_handle, (LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi)) { + if (mbi.State == MEM_COMMIT && + !(mbi.Protect & PAGE_NOACCESS) && + !(mbi.Protect & PAGE_GUARD)) + { + rcx::MemoryRegion region; + region.base = (uint64_t)mbi.BaseAddress; + region.size = mbi.RegionSize; + region.readable = true; + region.writable = (mbi.Protect & PAGE_READWRITE) || + (mbi.Protect & PAGE_WRITECOPY) || + (mbi.Protect & PAGE_EXECUTE_READWRITE) || + (mbi.Protect & PAGE_EXECUTE_WRITECOPY); + region.executable = (mbi.Protect & PAGE_EXECUTE) || + (mbi.Protect & PAGE_EXECUTE_READ) || + (mbi.Protect & PAGE_EXECUTE_READWRITE) || + (mbi.Protect & PAGE_EXECUTE_WRITECOPY); + + // Match module name from cached module list + for (const auto& mod : m_modules) { + if (region.base >= mod.base && region.base < mod.base + mod.size) { + region.moduleName = mod.name; + break; + } + } + + regions.append(region); + } + + uint64_t next = (uint64_t)mbi.BaseAddress + mbi.RegionSize; + if (next <= addr) break; // overflow protection + addr = next; + } + + return regions; +} + #elif defined(__linux__) ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName) @@ -282,6 +327,58 @@ void ProcessMemoryProvider::cacheModules() } } +QVector ProcessMemoryProvider::enumerateRegions() const +{ + QVector regions; + if (m_fd < 0) return regions; + + QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid); + std::ifstream mapsFile(mapsPath.toStdString()); + if (!mapsFile.is_open()) return regions; + + std::string line; + while (std::getline(mapsFile, line)) { + std::istringstream iss(line); + std::string addrRange, perms, offset, dev, inode, pathname; + iss >> addrRange >> perms >> offset >> dev >> inode; + std::getline(iss, pathname); + + auto dash = addrRange.find('-'); + if (dash == std::string::npos) continue; + uint64_t addrStart = std::stoull(addrRange.substr(0, dash), nullptr, 16); + uint64_t addrEnd = std::stoull(addrRange.substr(dash + 1), nullptr, 16); + + if (perms.size() < 4) continue; + bool readable = (perms[0] == 'r'); + bool writable = (perms[1] == 'w'); + bool executable = (perms[2] == 'x'); + + if (!readable) continue; + + rcx::MemoryRegion region; + region.base = addrStart; + region.size = addrEnd - addrStart; + region.readable = readable; + region.writable = writable; + region.executable = executable; + + // Extract module name from pathname + size_t start = pathname.find_first_not_of(" \t"); + if (start != std::string::npos) { + QString qpath = QString::fromStdString(pathname.substr(start)); + if (qpath.startsWith('/') && !qpath.startsWith("/dev/") && + !qpath.startsWith("/memfd:")) { + QFileInfo fi(qpath); + region.moduleName = fi.fileName(); + } + } + + regions.append(region); + } + + return regions; +} + #endif // platform uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h index e8c1a49..7dc5f5d 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.h +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -28,6 +28,7 @@ public: bool isLive() const override { return true; } uint64_t base() const override { return m_base; } + QVector enumerateRegions() const override; bool isReadable(uint64_t, int len) const override { #ifdef _WIN32 return m_handle && len >= 0; diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp index d13abc8..e8200af 100644 --- a/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp @@ -165,6 +165,10 @@ void WinDbgMemoryProvider::initInterfaces() qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr << "ptr=" << (void*)m_dataSpaces; + hr = m_client->QueryInterface(IID_IDebugDataSpaces2, (void**)&m_dataSpaces2); + qDebug() << "[WinDbg] IDebugDataSpaces2 hr=" << Qt::hex << (unsigned long)hr + << "ptr=" << (void*)m_dataSpaces2; + hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control); qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr << "ptr=" << (void*)m_control; @@ -251,10 +255,11 @@ WinDbgMemoryProvider::~WinDbgMemoryProvider() void WinDbgMemoryProvider::cleanup() { #ifdef _WIN32 - if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; } - if (m_control) { m_control->Release(); m_control = nullptr; } - if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; } - if (m_client) { m_client->Release(); m_client = nullptr; } + if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; } + if (m_control) { m_control->Release(); m_control = nullptr; } + if (m_dataSpaces2) { m_dataSpaces2->Release(); m_dataSpaces2 = nullptr; } + if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; } + if (m_client) { m_client->Release(); m_client = nullptr; } #endif } @@ -351,6 +356,112 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const #endif } +QVector WinDbgMemoryProvider::enumerateRegions() const +{ + QVector regions; +#ifdef _WIN32 + if (!m_dataSpaces) return regions; + + // Enumerate modules — used for tagging (user-mode) or as the primary + // source of regions (kernel-mode, where QueryVirtual is unavailable). + struct ModInfo { uint64_t base; uint64_t size; QString name; }; + QVector modules; + + if (m_symbols) { + dispatchToOwner([&]() { + ULONG loaded = 0, unloaded = 0; + if (FAILED(m_symbols->GetNumberModules(&loaded, &unloaded))) + return; + for (ULONG i = 0; i < loaded; i++) { + ULONG64 modBase = 0; + if (FAILED(m_symbols->GetModuleByIndex(i, &modBase))) + continue; + DEBUG_MODULE_PARAMETERS params = {}; + if (FAILED(m_symbols->GetModuleParameters(1, &modBase, 0, ¶ms))) + continue; + char nameBuf[256] = {}; + ULONG nameSize = 0; + m_symbols->GetModuleNames(i, 0, + nullptr, 0, nullptr, + nameBuf, sizeof(nameBuf), &nameSize, + nullptr, 0, nullptr); + ModInfo mi; + mi.base = modBase; + mi.size = params.Size; + mi.name = QString::fromUtf8(nameBuf); + modules.append(mi); + } + }); + } + + // Try QueryVirtual first (user-mode debugging / user-mode dumps). + // MSDN: "This method is not available in kernel-mode debugging." + if (m_dataSpaces2) { + dispatchToOwner([&]() { + ULONG64 addr = 0; + int safety = 0; + constexpr int kMaxRegions = 500000; + + while (safety++ < kMaxRegions) { + MEMORY_BASIC_INFORMATION64 mbi = {}; + HRESULT hr = m_dataSpaces2->QueryVirtual(addr, &mbi); + if (FAILED(hr)) + break; + + if (mbi.State == MEM_COMMIT && + !(mbi.Protect & PAGE_NOACCESS) && + !(mbi.Protect & PAGE_GUARD)) + { + rcx::MemoryRegion region; + region.base = mbi.BaseAddress; + region.size = mbi.RegionSize; + region.readable = true; + region.writable = (mbi.Protect & PAGE_READWRITE) || + (mbi.Protect & PAGE_WRITECOPY) || + (mbi.Protect & PAGE_EXECUTE_READWRITE) || + (mbi.Protect & PAGE_EXECUTE_WRITECOPY); + region.executable = (mbi.Protect & PAGE_EXECUTE) || + (mbi.Protect & PAGE_EXECUTE_READ) || + (mbi.Protect & PAGE_EXECUTE_READWRITE) || + (mbi.Protect & PAGE_EXECUTE_WRITECOPY); + + for (const auto& mod : modules) { + if (region.base >= mod.base && region.base < mod.base + mod.size) { + region.moduleName = mod.name; + break; + } + } + + regions.append(region); + } + + ULONG64 next = mbi.BaseAddress + mbi.RegionSize; + if (next <= addr) break; + addr = next; + } + }); + } + + // Fallback for kernel-mode debugging: QueryVirtual is unavailable, + // so use loaded modules as scannable regions. Each module image + // becomes one region — the scanner reads through module code/data. + if (regions.isEmpty() && !modules.isEmpty()) { + for (const auto& mod : modules) { + if (mod.size == 0) continue; + rcx::MemoryRegion region; + region.base = mod.base; + region.size = mod.size; + region.readable = true; + region.writable = false; + region.executable = true; + region.moduleName = mod.name; + regions.append(region); + } + } +#endif + return regions; +} + // ────────────────────────────────────────────────────────────────────────── // WinDbgMemoryPlugin implementation // ────────────────────────────────────────────────────────────────────────── @@ -408,7 +519,7 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target) { QDialog dlg(parent); dlg.setWindowTitle("WinDbg Settings"); - dlg.resize(460, 260); + dlg.resize(460, 300); QPalette dlgPal = qApp->palette(); dlg.setPalette(dlgPal); @@ -418,7 +529,9 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target) layout->addWidget(new QLabel( "Connect to a running WinDbg debug server.\n" - "In WinDbg, run: .server tcp:port=5055")); + "In WinDbg, run: .server tcp:port=5055\n\n" + "Non-invasive debug and dump files only.\n" + "Execution control (bp, g, t, p) is not supported.")); layout->addSpacing(8); layout->addWidget(new QLabel("Connection string:")); diff --git a/plugins/WinDbgMemory/WinDbgMemoryPlugin.h b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h index b2b5a4e..771b73a 100644 --- a/plugins/WinDbgMemory/WinDbgMemoryPlugin.h +++ b/plugins/WinDbgMemory/WinDbgMemoryPlugin.h @@ -9,6 +9,7 @@ // Forward declarations for DbgEng COM interfaces struct IDebugClient; struct IDebugDataSpaces; +struct IDebugDataSpaces2; struct IDebugControl; struct IDebugSymbols; @@ -59,6 +60,7 @@ public: QString name() const override { return m_name; } QString kind() const override { return QStringLiteral("WinDbg"); } QString getSymbol(uint64_t addr) const override; + QVector enumerateRegions() const override; bool isLive() const override { return m_isLive; } uint64_t base() const override { return m_base; } @@ -73,10 +75,11 @@ private: template void dispatchToOwner(Fn&& fn) const; - IDebugClient* m_client = nullptr; - IDebugDataSpaces* m_dataSpaces = nullptr; - IDebugControl* m_control = nullptr; - IDebugSymbols* m_symbols = nullptr; + IDebugClient* m_client = nullptr; + IDebugDataSpaces* m_dataSpaces = nullptr; + IDebugDataSpaces2* m_dataSpaces2 = nullptr; + IDebugControl* m_control = nullptr; + IDebugSymbols* m_symbols = nullptr; QString m_name; uint64_t m_base = 0; diff --git a/src/main.cpp b/src/main.cpp index b59b642..d5448e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -440,6 +440,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setCentralWidget(m_mdiArea); createWorkspaceDock(); + createScannerDock(); createMenus(); createStatusBar(); @@ -611,6 +612,11 @@ void MainWindow::createMenus() { view->addSeparator(); view->addAction(m_workspaceDock->toggleViewAction()); + { + auto* scanAct = m_scannerDock->toggleViewAction(); + scanAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_S)); + view->addAction(scanAct); + } // Tools auto* tools = m_titleBar->menuBar()->addMenu("&Tools"); @@ -1813,6 +1819,25 @@ void MainWindow::applyTheme(const Theme& theme) { "QToolButton:hover { color: %2; }") .arg(theme.textDim.name(), theme.indHoverSpan.name())); + // Scanner dock + if (m_scannerPanel) + m_scannerPanel->applyTheme(theme); + if (m_scanDockTitle) { + QPalette lp = m_scanDockTitle->palette(); + lp.setColor(QPalette::WindowText, theme.textDim); + m_scanDockTitle->setPalette(lp); + } + if (auto* titleBar = m_scannerDock ? m_scannerDock->titleBarWidget() : nullptr) { + QPalette tbPal = titleBar->palette(); + tbPal.setColor(QPalette::Window, theme.backgroundAlt); + titleBar->setPalette(tbPal); + } + if (m_scanDockCloseBtn) + m_scanDockCloseBtn->setStyleSheet(QStringLiteral( + "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" + "QToolButton:hover { color: %2; }") + .arg(theme.textDim.name(), theme.indHoverSpan.name())); + // Rendered C/C++ views: update lexer colors, paper, margins for (auto& tab : m_tabs) { for (auto& pane : tab.panes) { @@ -1933,6 +1958,11 @@ void MainWindow::setEditorFont(const QString& fontName) { // Sync dock titlebar font if (m_dockTitleLabel) m_dockTitleLabel->setFont(f); + // Sync scanner panel font + if (m_scannerPanel) + m_scannerPanel->setEditorFont(f); + if (m_scanDockTitle) + m_scanDockTitle->setFont(f); } RcxController* MainWindow::activeController() const { @@ -2814,6 +2844,87 @@ void MainWindow::createWorkspaceDock() { }); } +// ── Scanner Dock ── + +void MainWindow::createScannerDock() { + m_scannerDock = new QDockWidget("Scanner", this); + m_scannerDock->setObjectName("ScannerDock"); + m_scannerDock->setAllowedAreas( + Qt::BottomDockWidgetArea | Qt::RightDockWidgetArea | Qt::LeftDockWidgetArea); + m_scannerDock->setFeatures( + QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + // Custom titlebar: label + close button (matches workspace dock) + { + const auto& t = ThemeManager::instance().current(); + + auto* titleBar = new QWidget(m_scannerDock); + titleBar->setFixedHeight(24); + titleBar->setAutoFillBackground(true); + { + QPalette tbPal = titleBar->palette(); + tbPal.setColor(QPalette::Window, t.backgroundAlt); + titleBar->setPalette(tbPal); + } + auto* layout = new QHBoxLayout(titleBar); + layout->setContentsMargins(6, 2, 2, 2); + layout->setSpacing(0); + + m_scanDockTitle = new QLabel("Scanner", titleBar); + { + QPalette lp = m_scanDockTitle->palette(); + lp.setColor(QPalette::WindowText, t.textDim); + m_scanDockTitle->setPalette(lp); + } + layout->addWidget(m_scanDockTitle); + + layout->addStretch(); + + m_scanDockCloseBtn = new QToolButton(titleBar); + m_scanDockCloseBtn->setText(QStringLiteral("\u2715")); + m_scanDockCloseBtn->setAutoRaise(true); + m_scanDockCloseBtn->setCursor(Qt::PointingHandCursor); + m_scanDockCloseBtn->setStyleSheet(QStringLiteral( + "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" + "QToolButton:hover { color: %2; }") + .arg(t.textDim.name(), t.indHoverSpan.name())); + connect(m_scanDockCloseBtn, &QToolButton::clicked, m_scannerDock, &QDockWidget::close); + layout->addWidget(m_scanDockCloseBtn); + + m_scannerDock->setTitleBarWidget(titleBar); + } + + m_scannerPanel = new ScannerPanel(m_scannerDock); + m_scannerPanel->applyTheme(ThemeManager::instance().current()); + { + QSettings settings("Reclass", "Reclass"); + QString fontName = settings.value("font", "JetBrains Mono").toString(); + QFont f(fontName, 12); + f.setFixedPitch(true); + m_scannerPanel->setEditorFont(f); + m_scanDockTitle->setFont(f); + } + m_scannerDock->setWidget(m_scannerPanel); + addDockWidget(Qt::BottomDockWidgetArea, m_scannerDock); + m_scannerDock->hide(); + + // Wire provider getter: lazily captures the active tab's provider at scan time + m_scannerPanel->setProviderGetter([this]() -> std::shared_ptr { + auto* ctrl = activeController(); + return ctrl ? ctrl->document()->provider : nullptr; + }); + + // Wire "Go to Address" to rebase the active tab + connect(m_scannerPanel, &ScannerPanel::goToAddress, this, [this](uint64_t addr) { + auto* ctrl = activeController(); + if (!ctrl) return; + ctrl->document()->tree.baseAddress = addr; + ctrl->document()->tree.baseAddressFormula.clear(); + ctrl->refresh(); + }); +} + void MainWindow::rebuildAllDocs() { m_allDocs.clear(); for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) diff --git a/src/mainwindow.h b/src/mainwindow.h index 2fdf15d..b90b7ca 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -2,6 +2,7 @@ #include "controller.h" #include "titlebar.h" #include "pluginmanager.h" +#include "scannerpanel.h" #include #include #include @@ -152,6 +153,13 @@ private: void rebuildWorkspaceModel(); void updateBorderColor(const QColor& color); + // Scanner dock + QDockWidget* m_scannerDock = nullptr; + ScannerPanel* m_scannerPanel = nullptr; + QLabel* m_scanDockTitle = nullptr; + QToolButton* m_scanDockCloseBtn = nullptr; + void createScannerDock(); + protected: void changeEvent(QEvent* event) override; void resizeEvent(QResizeEvent* event) override; diff --git a/src/providers/provider.h b/src/providers/provider.h index 3a8271e..9004b24 100644 --- a/src/providers/provider.h +++ b/src/providers/provider.h @@ -1,11 +1,21 @@ #pragma once #include #include +#include #include #include namespace rcx { +struct MemoryRegion { + uint64_t base = 0; + uint64_t size = 0; + bool readable = true; + bool writable = false; + bool executable = false; + QString moduleName; +}; + class Provider { public: virtual ~Provider() = default; @@ -54,6 +64,11 @@ public: return 0; } + // Enumerate committed/readable memory regions. + // Used by the scan engine to know what address ranges to scan. + // Default: returns empty (scan engine falls back to [0, size())). + virtual QVector enumerateRegions() const { return {}; } + // --- Derived convenience (non-virtual, never override) --- bool isValid() const { return size() > 0; } diff --git a/src/scanner.cpp b/src/scanner.cpp index 3c55bd2..7d7bf79 100644 --- a/src/scanner.cpp +++ b/src/scanner.cpp @@ -1,8 +1,11 @@ #include "scanner.h" #include #include +#include +#include #include #include +#include namespace rcx { @@ -393,12 +396,20 @@ void ScanEngine::start(std::shared_ptr provider, const ScanRequest& re QVector ScanEngine::runScan(std::shared_ptr prov, const ScanRequest& req) { + QElapsedTimer timer; + timer.start(); + QVector results; if (!prov || req.pattern.isEmpty()) return results; auto regions = prov->enumerateRegions(); + qDebug() << "[scan] regions:" << regions.size() + << " pattern:" << req.pattern.size() << "bytes" + << " align:" << req.alignment + << " filterExec:" << req.filterExecutable + << " filterWrite:" << req.filterWritable; // Fallback for providers that don't enumerate regions (file/buffer) if (regions.isEmpty()) { @@ -424,6 +435,8 @@ QVector ScanEngine::runScan(std::shared_ptr prov, totalBytes += r.size; } + qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions"; + if (totalBytes == 0) return results; uint64_t scannedBytes = 0; @@ -473,6 +486,7 @@ QVector ScanEngine::runScan(std::shared_ptr prov, ScanResult r; r.address = region.base + off + (uint64_t)i; r.regionModule = region.moduleName; + r.scanValue = QByteArray(data + i, qMin(16, readLen - i)); results.append(r); if (results.size() >= req.maxResults) @@ -501,6 +515,100 @@ QVector ScanEngine::runScan(std::shared_ptr prov, } done: + qDebug() << "[scan] done:" << results.size() << "results in" << timer.elapsed() << "ms" + << " scanned:" << (scannedBytes / 1024) << "KB"; + return results; +} + +void ScanEngine::startRescan(std::shared_ptr provider, + QVector results, int readSize) { + if (isRunning()) return; + + m_abort.store(false); + + auto* watcher = new QFutureWatcher>(this); + m_watcher = watcher; + + connect(watcher, &QFutureWatcher>::finished, this, [this, watcher]() { + auto results = watcher->result(); + watcher->deleteLater(); + if (m_watcher == watcher) + m_watcher = nullptr; + emit rescanFinished(results); + }); + + watcher->setFuture(QtConcurrent::run( + [this, provider, results = std::move(results), readSize]() mutable { + return runRescan(provider, std::move(results), readSize); + })); +} + +QVector ScanEngine::runRescan(std::shared_ptr prov, + QVector results, int readSize) { + QElapsedTimer timer; + timer.start(); + + int total = results.size(); + if (total == 0 || !prov) return results; + + qDebug() << "[rescan] start: " << total << "results, readSize:" << readSize; + + // Save previous values + for (auto& r : results) + r.previousValue = r.scanValue; + + // Sort indices by address for sequential chunked reads + QVector order(total); + for (int i = 0; i < total; i++) order[i] = i; + std::sort(order.begin(), order.end(), [&results](int a, int b) { + return results[a].address < results[b].address; + }); + + constexpr int kChunk = 256 * 1024; + int updated = 0; + int lastPct = -1; + int chunks = 0; + uint64_t totalBytesRead = 0; + int i = 0; + + while (i < total && !m_abort.load()) { + uint64_t spanBase = results[order[i]].address; + int spanEnd = i; + + // Extend span while next result fits in the same chunk + while (spanEnd + 1 < total) { + uint64_t endAddr = results[order[spanEnd + 1]].address + readSize; + if (endAddr - spanBase > (uint64_t)kChunk) break; + spanEnd++; + } + + uint64_t spanLast = results[order[spanEnd]].address; + int chunkLen = (int)(spanLast + readSize - spanBase); + QByteArray chunk(chunkLen, '\0'); + prov->read(spanBase, chunk.data(), chunkLen); + + for (int j = i; j <= spanEnd; j++) { + auto& r = results[order[j]]; + int off = (int)(r.address - spanBase); + r.scanValue = chunk.mid(off, readSize); + } + + chunks++; + totalBytesRead += chunkLen; + updated += (spanEnd - i + 1); + i = spanEnd + 1; + + int pct = updated * 100 / total; + if (pct != lastPct) { + lastPct = pct; + QMetaObject::invokeMethod(this, "progress", + Qt::QueuedConnection, Q_ARG(int, pct)); + } + } + + qDebug() << "[rescan] done:" << updated << "/" << total << "results in" + << timer.elapsed() << "ms |" << chunks << "chunks," + << (totalBytesRead / 1024) << "KB read"; return results; } diff --git a/src/scanner.h b/src/scanner.h index 864e78c..6d2592d 100644 --- a/src/scanner.h +++ b/src/scanner.h @@ -65,16 +65,21 @@ public: explicit ScanEngine(QObject* parent = nullptr); void start(std::shared_ptr provider, const ScanRequest& req); + void startRescan(std::shared_ptr provider, + QVector results, int readSize); void abort(); bool isRunning() const; signals: void progress(int percent); void finished(QVector results); + void rescanFinished(QVector results); void error(QString message); private: QVector runScan(std::shared_ptr prov, const ScanRequest& req); + QVector runRescan(std::shared_ptr prov, + QVector results, int readSize); std::atomic m_abort{false}; QFutureWatcher>* m_watcher = nullptr; diff --git a/src/scannerpanel.cpp b/src/scannerpanel.cpp index dba6a28..951754c 100644 --- a/src/scannerpanel.cpp +++ b/src/scannerpanel.cpp @@ -1,6 +1,8 @@ #include "scannerpanel.h" #include "addressparser.h" #include +#include +#include #include #include #include @@ -135,8 +137,8 @@ ScannerPanel::ScannerPanel(QWidget* parent) 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->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive); + m_resultTable->horizontalHeader()->setStretchLastSection(true); m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows); m_resultTable->setSelectionMode(QAbstractItemView::SingleSelection); m_resultTable->setEditTriggers(QAbstractItemView::DoubleClicked); @@ -169,11 +171,12 @@ ScannerPanel::ScannerPanel(QWidget* parent) mainLayout->addLayout(actionRow); - // ── Initial visibility: signature mode ── + // ── Initial state: signature mode ── m_typeLabel->hide(); m_typeCombo->hide(); m_valueLabel->hide(); m_valueEdit->hide(); + m_execCheck->setChecked(true); // ── Connections ── connect(m_modeCombo, QOverload::of(&QComboBox::currentIndexChanged), @@ -225,6 +228,8 @@ ScannerPanel::ScannerPanel(QWidget* parent) }); connect(m_engine, &ScanEngine::finished, this, &ScannerPanel::onScanFinished); + connect(m_engine, &ScanEngine::rescanFinished, + this, &ScannerPanel::onRescanFinished); connect(m_engine, &ScanEngine::error, this, [this](const QString& msg) { m_statusLabel->setText(QStringLiteral("Error: %1").arg(msg)); m_scanBtn->setText(QStringLiteral("Scan")); @@ -240,6 +245,8 @@ void ScannerPanel::setEditorFont(const QFont& font) { m_resultTable->setFont(font); QFontMetrics fm(font); m_resultTable->verticalHeader()->setDefaultSectionSize(fm.height() + 6); + // Address column width: "00000000`00000000" + padding + m_resultTable->setColumnWidth(0, fm.horizontalAdvance(QStringLiteral("00000000`00000000")) + 20); m_patternEdit->setFont(font); m_valueEdit->setFont(font); m_modeCombo->setFont(font); @@ -275,15 +282,16 @@ void ScannerPanel::onModeChanged(int index) { m_typeCombo->setVisible(!isSig); m_valueLabel->setVisible(!isSig); m_valueEdit->setVisible(!isSig); + + // Auto-toggle filters: signatures → executable code, values → writable data + m_execCheck->setChecked(isSig); + m_writeCheck->setChecked(!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; + return; // finished/rescanFinished handler resets UI } // Get provider @@ -344,41 +352,40 @@ ScanRequest ScannerPanel::buildRequest() { void ScannerPanel::onScanFinished(QVector results) { m_scanBtn->setText(QStringLiteral("Scan")); m_progressBar->hide(); - m_results = results; + m_results = std::move(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(); + // Bytes are cached by the engine during scan. + // Value mode: override with exact search pattern (engine caches raw chunk bytes). + for (auto& r : m_results) { + r.previousValue.clear(); + if (m_lastScanMode == 1) r.scanValue = m_lastPattern; - } - } else { - // Signature mode — wildcards mean each match may differ, read actual bytes - std::shared_ptr 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); + { + QElapsedTimer pt; + pt.start(); + populateTable(false); + qDebug() << "[panel] populateTable(initial):" << m_results.size() + << "results," << pt.elapsed() << "ms"; + } + int n = m_results.size(); m_statusLabel->setText(QStringLiteral("%1 result%2") - .arg(m_results.size()) - .arg(m_results.size() == 1 ? "" : "s")); + .arg(n).arg(n == 1 ? "" : "s")); } void ScannerPanel::populateTable(bool showPrevious) { + constexpr int kMaxRows = 10000; + m_resultTable->blockSignals(true); int cols = showPrevious ? 3 : 2; m_resultTable->setColumnCount(cols); - m_resultTable->setRowCount(m_results.size()); + int displayCount = qMin(m_results.size(), kMaxRows); + m_resultTable->setRowCount(displayCount); - for (int i = 0; i < m_results.size(); i++) { + for (int i = 0; i < displayCount; i++) { const auto& r = m_results[i]; // Address column — WinDbg backtick format: 00000000`00000000 @@ -402,13 +409,11 @@ void ScannerPanel::populateTable(bool showPrevious) { } } - m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - m_resultTable->horizontalHeader()->setStretchLastSection(false); m_resultTable->blockSignals(false); } void ScannerPanel::onUpdateClicked() { - if (m_results.isEmpty()) return; + if (m_results.isEmpty() || m_engine->isRunning()) return; std::shared_ptr prov; if (m_providerGetter) @@ -418,55 +423,34 @@ void ScannerPanel::onUpdateClicked() { return; } + int readSize = (m_lastScanMode == 1) ? valueSize() : 16; + m_updateBtn->setEnabled(false); - m_scanBtn->setEnabled(false); + m_scanBtn->setText(QStringLiteral("Cancel")); 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(); + m_engine->startRescan(prov, m_results, readSize); +} - // Single pass: read new values + build table rows - m_resultTable->blockSignals(true); - m_resultTable->setColumnCount(3); - m_resultTable->setRowCount(total); +void ScannerPanel::onRescanFinished(QVector results) { + m_scanBtn->setText(QStringLiteral("Scan")); + m_progressBar->hide(); + m_results = std::move(results); + m_updateBtn->setEnabled(!m_results.isEmpty()); - 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(); - } + { + QElapsedTimer pt; + pt.start(); + populateTable(true); + qDebug() << "[panel] populateTable(rescan):" << m_results.size() + << "results," << pt.elapsed() << "ms"; } - 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); + int n = m_results.size(); m_statusLabel->setText(QStringLiteral("Updated %1 result%2") - .arg(total).arg(total == 1 ? "" : "s")); + .arg(n).arg(n == 1 ? "" : "s")); } void ScannerPanel::onGoToAddress() { diff --git a/src/scannerpanel.h b/src/scannerpanel.h index 9e48ed5..a477662 100644 --- a/src/scannerpanel.h +++ b/src/scannerpanel.h @@ -65,6 +65,7 @@ private slots: void onResultDoubleClicked(int row, int col); void onCellEdited(int row, int col); void onUpdateClicked(); + void onRescanFinished(QVector results); private: ScanRequest buildRequest(); diff --git a/tests/test_scanner_ui.cpp b/tests/test_scanner_ui.cpp index e7fc8b9..7676e9b 100644 --- a/tests/test_scanner_ui.cpp +++ b/tests/test_scanner_ui.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "scannerpanel.h" #include "scanner.h" @@ -29,6 +30,11 @@ private slots: void init() { m_panel = new ScannerPanel(); m_panel->show(); + // Clear mode-dependent filter defaults so BufferProvider scans + // (which have no executable regions) work without filter issues. + // The initialState_filterCheckboxes test verifies defaults separately. + m_panel->execCheck()->setChecked(false); + m_panel->writeCheck()->setChecked(false); QApplication::processEvents(); } @@ -77,8 +83,19 @@ private slots: } void initialState_filterCheckboxes() { - QVERIFY(!m_panel->execCheck()->isChecked()); - QVERIFY(!m_panel->writeCheck()->isChecked()); + // Verify defaults on a fresh panel (init() clears filters for test convenience) + ScannerPanel fresh; + // Signature mode default: executable checked, writable unchecked + QVERIFY(fresh.execCheck()->isChecked()); + QVERIFY(!fresh.writeCheck()->isChecked()); + // Switching to value mode flips them + fresh.modeCombo()->setCurrentIndex(1); + QVERIFY(!fresh.execCheck()->isChecked()); + QVERIFY(fresh.writeCheck()->isChecked()); + // Switching back restores signature defaults + fresh.modeCombo()->setCurrentIndex(0); + QVERIFY(fresh.execCheck()->isChecked()); + QVERIFY(!fresh.writeCheck()->isChecked()); } void initialState_patternPlaceholder() { @@ -774,8 +791,10 @@ private slots: std::memcpy(newBytes.data(), &newVal, 4); prov->writeBytes(8, newBytes); - // Click update + // Click update — runs async + QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); + QVERIFY(rescanSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->columnCount(), 3); // Current value = 99, previous = 50 @@ -821,16 +840,16 @@ private slots: prov->writeBytes(i * 4, nb); } - // Click Re-scan + // Click Re-scan — runs async + QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); + QVERIFY(rescanSpy.wait(5000)); 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")); @@ -855,6 +874,7 @@ private slots: m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(0); // Signature + m_panel->execCheck()->setChecked(false); // BufferProvider has no exec regions m_panel->patternEdit()->setText("48 8B ??"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); @@ -867,8 +887,10 @@ private slots: mod[0] = 0x48; mod[1] = 0x8B; mod[2] = (char)0xFF; prov->writeBytes(0, mod); - // Re-scan + // Re-scan — runs async + QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); + QVERIFY(rescanSpy.wait(5000)); QApplication::processEvents(); QVERIFY(!m_panel->progressBar()->isVisible()); @@ -908,8 +930,12 @@ private slots: QByteArray nb2(4, '\0'); std::memcpy(nb2.data(), &v2, 4); prov->writeBytes(4, nb2); - QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); - QApplication::processEvents(); + { + QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); + QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); + QVERIFY(rescanSpy.wait(5000)); + QApplication::processEvents(); + } QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("20")); QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("10")); @@ -918,8 +944,12 @@ private slots: QByteArray nb3(4, '\0'); std::memcpy(nb3.data(), &v3, 4); prov->writeBytes(4, nb3); - QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); - QApplication::processEvents(); + { + QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); + QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); + QVERIFY(rescanSpy.wait(5000)); + QApplication::processEvents(); + } QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("30")); QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("20")); } @@ -928,6 +958,151 @@ private slots: // Provider getter is lazy (captures at scan time) // ═══════════════════════════════════════════════════════════════════ + // ═══════════════════════════════════════════════════════════════════ + // Benchmark: initial scan + 10 re-scans on a large buffer + // ═══════════════════════════════════════════════════════════════════ + + void bench_rescan10x() { + // 1MB buffer, int32 value=42 placed every 512 bytes → 2048 results + constexpr int kBufSize = 1 * 1024 * 1024; + constexpr int kStride = 512; + constexpr int32_t kVal = 42; + + QByteArray data(kBufSize, '\0'); + int planted = 0; + for (int off = 0; off + 4 <= kBufSize; off += kStride) { + std::memcpy(data.data() + off, &kVal, 4); + planted++; + } + qDebug() << "[bench] buffer:" << (kBufSize / 1024) << "KB," + << planted << "planted values, stride:" << kStride; + + auto prov = std::make_shared(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(QString::number(kVal)); + + // ── Initial scan ── + QElapsedTimer totalTimer; + totalTimer.start(); + + QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); + QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); + QVERIFY(finSpy.wait(30000)); + QApplication::processEvents(); + + int resultCount = m_panel->resultsTable()->rowCount(); + qDebug() << "[bench] initial scan:" << totalTimer.elapsed() << "ms," + << resultCount << "results displayed"; + QVERIFY(resultCount > 0); + QVERIFY(m_panel->updateButton()->isEnabled()); + + // ── 10 re-scans — mutate values each time ── + for (int iter = 1; iter <= 10; iter++) { + int32_t newVal = kVal + iter; + for (int off = 0; off + 4 <= kBufSize; off += kStride) + std::memcpy(prov->data().data() + off, &newVal, 4); + + QElapsedTimer iterTimer; + iterTimer.start(); + + QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); + QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); + QVERIFY2(rescanSpy.wait(30000), + qPrintable(QString("rescan #%1 timed out").arg(iter))); + QApplication::processEvents(); + + qDebug() << "[bench] rescan #" << iter << ":" << iterTimer.elapsed() << "ms" + << "| val:" << newVal + << "| rows:" << m_panel->resultsTable()->rowCount() + << "| status:" << m_panel->statusLabel()->text(); + + QVERIFY(m_panel->resultsTable()->item(0, 1) != nullptr); + QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), + QString::number(newVal)); + if (m_panel->resultsTable()->columnCount() >= 3) { + QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), + QString::number(newVal - 1)); + } + QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan")); + QVERIFY(!m_panel->progressBar()->isVisible()); + QVERIFY(m_panel->updateButton()->isEnabled()); + } + + qDebug() << "[bench] total (scan + 10 rescans):" << totalTimer.elapsed() << "ms"; + } + + // ═══════════════════════════════════════════════════════════════════ + // Benchmark: signature re-scan (16 bytes per result) + // ═══════════════════════════════════════════════════════════════════ + + void bench_rescanSignature10x() { + // 1MB buffer with "MZ" planted every 4096 bytes → 256 results + constexpr int kBufSize = 1 * 1024 * 1024; + constexpr int kStride = 4096; + + QByteArray data(kBufSize, '\0'); + int planted = 0; + for (int off = 0; off + 2 <= kBufSize; off += kStride) { + data[off] = 0x4D; data[off + 1] = 0x5A; + planted++; + } + qDebug() << "[bench-sig] buffer:" << (kBufSize / 1024) << "KB," + << planted << "planted sigs"; + + auto prov = std::make_shared(data); + m_panel->setProviderGetter([prov]() { return prov; }); + + m_panel->modeCombo()->setCurrentIndex(0); + m_panel->patternEdit()->setText("4D 5A"); + + QElapsedTimer totalTimer; + totalTimer.start(); + + QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); + QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); + QVERIFY(finSpy.wait(30000)); + QApplication::processEvents(); + + qDebug() << "[bench-sig] initial scan:" << totalTimer.elapsed() << "ms," + << m_panel->resultsTable()->rowCount() << "results"; + QVERIFY(m_panel->resultsTable()->rowCount() > 0); + + for (int iter = 1; iter <= 10; iter++) { + for (int off = 0; off + 3 <= kBufSize; off += kStride) + prov->data().data()[off + 2] = (char)(iter & 0xFF); + + QElapsedTimer iterTimer; + iterTimer.start(); + + QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); + QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); + QVERIFY2(rescanSpy.wait(30000), + qPrintable(QString("sig rescan #%1 timed out").arg(iter))); + QApplication::processEvents(); + + qDebug() << "[bench-sig] rescan #" << iter << ":" << iterTimer.elapsed() << "ms" + << "| status:" << m_panel->statusLabel()->text(); + + QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan")); + QVERIFY(!m_panel->progressBar()->isVisible()); + QVERIFY(m_panel->updateButton()->isEnabled()); + } + + qDebug() << "[bench-sig] total:" << totalTimer.elapsed() << "ms"; + } + + // ═══════════════════════════════════════════════════════════════════ + // Provider getter is lazy (captures at scan time) + // ═══════════════════════════════════════════════════════════════════ + void providerGetter_lazy() { auto prov1 = std::make_shared(QByteArray(16, '\xAA')); auto prov2 = std::make_shared(QByteArray(16, '\xBB')); diff --git a/tests/test_windbg_provider.cpp b/tests/test_windbg_provider.cpp index 3e7e80f..c5686c7 100644 --- a/tests/test_windbg_provider.cpp +++ b/tests/test_windbg_provider.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include "providers/provider.h" +#include "scanner.h" #include "../plugins/WinDbgMemory/WinDbgMemoryPlugin.h" #ifdef _WIN32 @@ -481,6 +483,128 @@ private slots: delete raw; } + // ── enumerateRegions ── + + void provider_enumerateRegions() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + auto regions = prov.enumerateRegions(); + qDebug() << "enumerateRegions returned" << regions.size() << "regions"; + QVERIFY2(!regions.isEmpty(), "Should return at least one memory region"); + + // Every region should have sane values + for (const auto& r : regions) { + QVERIFY(r.size > 0); + QVERIFY(r.readable); + } + } + + void provider_enumerateRegions_hasModuleNames() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + auto regions = prov.enumerateRegions(); + QVERIFY(!regions.isEmpty()); + + // At least one region should have a module name + bool hasModule = false; + for (const auto& r : regions) { + if (!r.moduleName.isEmpty()) { + hasModule = true; + qDebug() << "Region base=0x" + QString::number(r.base, 16) + << "size=" << r.size + << "module=" << r.moduleName + << "r/w/x:" << r.readable << r.writable << r.executable; + break; + } + } + QVERIFY2(hasModule, "At least one region should have a module name"); + } + + void provider_enumerateRegions_hasExecutable() + { + WinDbgMemoryProvider prov(m_connString); + QVERIFY(prov.isValid()); + + auto regions = prov.enumerateRegions(); + QVERIFY(!regions.isEmpty()); + + bool hasExec = false; + for (const auto& r : regions) { + if (r.executable) { hasExec = true; break; } + } + QVERIFY2(hasExec, "Should have at least one executable region (code)"); + } + + // ── Scanner integration ── + + void scanner_signature_mz() + { + // Scan for the MZ header — should find at least one match + auto prov = std::make_shared(m_connString); + QVERIFY(prov->isValid()); + + auto regions = prov->enumerateRegions(); + QVERIFY2(!regions.isEmpty(), "Need regions for scan"); + + rcx::ScanRequest req; + QString err; + QVERIFY(rcx::parseSignature("4D 5A", req.pattern, req.mask, &err)); + req.alignment = 1; + req.maxResults = 100; + + rcx::ScanEngine engine; + QSignalSpy spy(&engine, &rcx::ScanEngine::finished); + + engine.start(prov, req); + QVERIFY(spy.wait(30000)); + + auto results = spy.at(0).at(0).value>(); + qDebug() << "MZ scan found" << results.size() << "results"; + QVERIFY2(!results.isEmpty(), "Should find at least one MZ header"); + + // Verify the first result is actually 'MZ' + uint8_t buf[2] = {}; + prov->read(results[0].address, buf, 2); + QCOMPARE(buf[0], (uint8_t)'M'); + QCOMPARE(buf[1], (uint8_t)'Z'); + } + + void scanner_value_int32() + { + // Read a known 4-byte value from offset 0x3C (PE offset) then scan for it. + // This only works for user-mode targets where address 0 is the main module. + auto prov = std::make_shared(m_connString); + QVERIFY(prov->isValid()); + + auto regions = prov->enumerateRegions(); + QVERIFY2(!regions.isEmpty(), "Need regions for scan"); + + uint32_t peOffset = prov->readU32(0x3C); + if (peOffset == 0 || peOffset >= 0x1000) + QSKIP("Address 0 not readable (kernel session) — value scan test requires user-mode target"); + + rcx::ScanRequest req; + QString err; + QVERIFY(rcx::serializeValue(rcx::ValueType::UInt32, + QString::number(peOffset), req.pattern, req.mask, &err)); + req.alignment = 4; + req.maxResults = 100; + + rcx::ScanEngine engine; + QSignalSpy spy(&engine, &rcx::ScanEngine::finished); + + engine.start(prov, req); + QVERIFY(spy.wait(30000)); + + auto results = spy.at(0).at(0).value>(); + qDebug() << "Value scan for" << peOffset << "found" << results.size() << "results"; + QVERIFY2(!results.isEmpty(), "Should find the PE offset value somewhere"); + } + // ── Kernel/dump session tests ── // Set WINDBG_KERNEL_CONN to a target string: // "dump:F:/path/to/file.dmp" — open dump directly @@ -500,7 +624,7 @@ private slots: WinDbgMemoryProvider prov(target); QVERIFY2(prov.isValid(), - qPrintable("Should connect, lastError: " + prov.lastError())); + qPrintable("Should connect to " + target)); QCOMPARE(prov.kind(), QStringLiteral("WinDbg")); qDebug() << "Kernel provider name:" << prov.name(); @@ -520,7 +644,7 @@ private slots: WinDbgMemoryProvider prov(target); QVERIFY2(prov.isValid(), - qPrintable("lastError: " + prov.lastError())); + qPrintable("Failed to connect to " + target)); bool ok = false; uint64_t addr = addrStr.toULongLong(&ok, 16); @@ -560,7 +684,7 @@ private slots: WinDbgMemoryProvider prov(target); QVERIFY2(prov.isValid(), - qPrintable("lastError: " + prov.lastError())); + qPrintable("Failed to connect to " + target)); if (addr == 0) addr = prov.base(); if (addr == 0) @@ -589,6 +713,67 @@ private slots: .arg(buf[7], 2, 16, QChar('0')); } + void provider_kernel_enumerateRegions() + { + QString target = kernelTarget(); + if (target.isEmpty()) + QSKIP("Set WINDBG_KERNEL_CONN"); + + WinDbgMemoryProvider prov(target); + QVERIFY2(prov.isValid(), + qPrintable("Failed to connect to " + target)); + + auto regions = prov.enumerateRegions(); + qDebug() << "Kernel enumerateRegions returned" << regions.size() << "regions"; + QVERIFY2(!regions.isEmpty(), "Should return kernel memory regions"); + + // Log first few regions + int logged = 0; + for (const auto& r : regions) { + if (logged++ >= 10) break; + qDebug() << " base=0x" + QString::number(r.base, 16) + << "size=" << r.size + << "module=" << r.moduleName + << "r/w/x:" << r.readable << r.writable << r.executable; + } + } + + void provider_kernel_scan_signature() + { + QString target = kernelTarget(); + if (target.isEmpty()) + QSKIP("Set WINDBG_KERNEL_CONN"); + + auto prov = std::make_shared(target); + QVERIFY2(prov->isValid(), + qPrintable("Failed to connect to " + target)); + + auto regions = prov->enumerateRegions(); + if (regions.isEmpty()) + QSKIP("No regions enumerated — QueryVirtual may not be supported for this target"); + + // Scan for MZ header in executable regions + rcx::ScanRequest req; + QString err; + QVERIFY(rcx::parseSignature("4D 5A 90 00", req.pattern, req.mask, &err)); + req.alignment = 1; + req.filterExecutable = true; + req.maxResults = 50; + + rcx::ScanEngine engine; + QSignalSpy spy(&engine, &rcx::ScanEngine::finished); + + engine.start(prov, req); + QVERIFY(spy.wait(60000)); + + auto results = spy.at(0).at(0).value>(); + qDebug() << "Kernel MZ scan (exec only) found" << results.size() << "results"; + for (const auto& r : results) + qDebug() << " 0x" + QString::number(r.address, 16) << r.regionModule; + + QVERIFY2(!results.isEmpty(), "Should find MZ headers in kernel modules"); + } + void provider_kernel_read_backgroundThread() { QString target = kernelTarget(); @@ -605,7 +790,7 @@ private slots: WinDbgMemoryProvider prov(target); QVERIFY2(prov.isValid(), - qPrintable("lastError: " + prov.lastError())); + qPrintable("Failed to connect to " + target)); // Simulate the controller's async refresh pattern QFuture future = QtConcurrent::run([&prov, addr]() -> QByteArray {