Compare commits

..

17 Commits

Author SHA1 Message Date
IChooseYou
7688bb5b92 ci: add SDK include paths for WDK NuGet driver build
NuGet splits WDK and SDK into separate packages. specstrings.h
lives in the SDK shared headers. Add SDK_INC_ROOT for shared/ucrt.
2026-03-14 05:40:24 -06:00
IChooseYou
701e088be8 ci: install WDK via NuGet for driver build in CI
Runner doesn't have WDK headers installed. Use NuGet to install
Microsoft.Windows.WDK.x64 and pass paths via env vars.
build_driver.bat now accepts WDK_INC_ROOT/WDK_LIB_ROOT overrides.
2026-03-14 05:20:23 -06:00
IChooseYou
3c0c248d54 fix: use delayed expansion in build_driver.bat for CI
Parentheses in "Program Files (x86)" broke cmd parser inside
for loop bodies. Switch to !var! delayed expansion.
2026-03-14 04:56:46 -06:00
IChooseYou
7af969f6bd ci: build kernel driver and include rcxdrv.sys in release
Add build_driver.bat step to Windows CI using runner's MSVC + WDK.
Copy rcxdrv.sys into Plugins/ in the release zip.
2026-03-13 15:27:51 -06:00
IChooseYou
8ba1fd2492 fix: auto-detect MSVC and WDK paths in build_driver.bat
Remove hardcoded MSVC 14.39.33519 and WDK 10.0.22621.0 paths.
Now scans for the newest installed version automatically.
2026-03-13 15:05:59 -06:00
IChooseYou
b08736245b feat: kernel memory plugin + unified source menu + driver improvements
- KernelMemory plugin: kernel-mode process/physical memory R/W via IOCTL driver
- rcxdrv.sys: MmCopyMemory for reads, MDL mapping with correct cache types
  (MmCached for RAM, MmNonCached for MMIO only — fixes cache corruption BSOD)
- Driver reconnect: ensureDriverLoaded tries device handle first, no auto
  stop+delete cycle. Manual unload closes handle only, service stays running.
- Unified source menu: ProviderRegistry::populateSourceMenu() shared by both
  main window Data Source menu and RcxEditor inline picker (icons + dll names)
- IProviderPlugin::populatePluginMenu() for conditional plugin actions
  (e.g. "Unload Kernel Driver" only when loaded)
- Physical memory mode removed from selectTarget (access via context menu only)
- requestOpenProviderTab sets base address from provider after template load
- Address parser: vtop(), cr3(), physRead() callbacks for kernel paging expressions
2026-03-13 14:46:22 -06:00
IChooseYou
7f7bbdcc45 fix: remove isRelative references from generator.cpp
Node::isRelative is not yet in the pushed core.h, breaking CI builds.
2026-03-12 18:07:34 -06:00
IChooseYou
79b5125229 fix: remove unreleased isRelative/ToggleRelative references from controller
These were local-only changes that referenced cmd::ToggleRelative and
Node::isRelative which don't exist in the pushed core.h, breaking CI.
2026-03-12 16:30:59 -06:00
IChooseYou
3aeb1a80d5 feat: inline hex byte and ASCII preview editors for hex nodes
Right-click context menu adds "Edit Hex Bytes" and "Edit ASCII" for
hex nodes (Hex8/16/32/64). Both are fixed-length overwrite-mode editors
with space-skipping, input validation, and IND_HEX_DIM indicator
preservation.
2026-03-11 16:01:37 -06:00
IChooseYou
3b7ed682ac Merge commit 'refs/pull/11/head' of github.com:IChooseYou/Reclass
# Conflicts:
#	src/mcp/mcp_bridge.cpp
2026-03-10 16:02:12 -06:00
IChooseYou
0582cb286b fix: commit missing selectPage() for OptionsDialog 2026-03-10 15:43:27 -06:00
IChooseYou
ea85b7a621 feat: add C# and Python ctypes code generators
- C# backend: [StructLayout(LayoutKind.Explicit)] with [FieldOffset], IntPtr pointers, fixed arrays, enums
- Python backend: ctypes.Structure with _fields_, POINTER() for typed pointers, c_void_p, padding
- Both support enums, vectors, bitfields, arrays, unions, static fields
- Export menu: C# Structs... and Python ctypes... entries
- Format combo auto-populates new options
- 14 new tests for both backends (all passing)
2026-03-10 15:20:56 -06:00
IChooseYou
6c8b7d3d97 feat: Rust/#define generators, code tab format/scope combos, enum #define support
- Add Rust #[repr(C)] and #define offset code generators with dispatch
- Add format combo + scope combo + gear button as corner widget on Code tab
- Corner controls hidden on Reclass tab, shown only on Code tab
- Chevron-down SVG arrows on combo dropdowns for consistent styling
- Fix enum #define output: emit named members instead of empty 0x0 struct
2026-03-10 15:05:23 -06:00
IChooseYou
d1321b5165 fix: per-group sentinel docks, editor inline-edit comment alignment
Sentinel dock refactored to per-tab-group model — each split group gets
its own hidden sentinel so tab bars stay visible without the Hide event
filter hack.  Editor inline-edit comment column now anchors correctly
for base-address edits and shows expression hint instead of generic text.
2026-03-09 11:44:55 -06:00
noita-player
4d0782db68 MCP bridge: support multiple concurrent clients
Replace single-client model (m_client/m_readBuffer/m_initialized)
with a ClientState vector. Each client gets its own read buffer and
initialized flag. Responses route to m_currentSender (set during
request processing); notifications broadcast to all initialized
clients.

Re-entrancy guard in onReadyRead: re-resolve ClientState after each
processLine() call since sendJson flush can re-enter the event loop
and trigger onDisconnected, removing the client mid-iteration.

Tests: 378-line test_mcp exercising connect, initialize, tools/list,
disconnect one client, notification broadcast, and serial requests
against a MockMcpServer with the same multi-client architecture.
2026-03-08 22:44:47 -07:00
noita-player
51de48a6ed Add MCP scanner tools, source.modules, reconnect, and constraint regions
Scanner engine:
- Add constrainRegions to ScanRequest — callers pass address ranges
  that are intersected with provider regions before scanning
- Merge overlapping/adjacent constraints to prevent duplicate results
- Fix final-chunk overlap: skip overlap advance on last chunk to avoid
  re-scanning the tail of a region

MCP tools:
- scanner.scan: value scans (int/float types) with optional region
  constraints, returns first 15 addresses
- scanner.scan_pattern: pattern/signature scans with wildcards
- source.modules: list loaded modules with base address and size
- mcp.reconnect: graceful client disconnect for IDE reconnection
- parseInteger() helper for hex string args (avoids JSON double
  precision loss on 64-bit addresses)
- Fix baseRelative semantics in hex.read/hex.write (was inverted)
- Auto-set tree.baseAddress from provider after process attach

Scanner panel:
- runValueScanAndWait() and runPatternScanAndWait() for blocking
  scan execution from MCP/automation code

Tests: 41 new test cases for constrainRegions covering gaps, partial
overlap, adjacent regions, writable filter, degenerate ranges,
overlapping constraints, boundary patterns, alignment, and value
types at region start/end positions.
2026-03-08 22:44:46 -07:00
noita-player
7b9b140823 Fix MCP use-after-free, scanner chunk overlap, build scripts
- MCP bridge: guard against use-after-free when client disconnects
  during sendJson flush by re-checking m_client after write
- Scanner engine: fix chunk overlap advancing past region end on
  final chunk; fix fallback region flags for providers without
  enumerateRegions
- Build scripts: prefer GCC MinGW over LLVM-MinGW in PATH detection
2026-03-08 22:44:36 -07:00
40 changed files with 7011 additions and 287 deletions

View File

@@ -41,6 +41,36 @@ jobs:
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
cmake --build build
- name: Install WDK NuGet
shell: pwsh
run: |
nuget install Microsoft.Windows.WDK.x64 -OutputDirectory wdk_pkg
$ntddk = Get-ChildItem wdk_pkg -Recurse -Filter "ntddk.h" |
Where-Object { $_.DirectoryName -like "*km*" } |
Select-Object -First 1
if (!$ntddk) { throw "ntddk.h not found in WDK NuGet package" }
$kmDir = $ntddk.DirectoryName
$incRoot = Split-Path $kmDir -Parent
Write-Host "WDK include root: $incRoot"
echo "WDK_INC_ROOT=$incRoot" >> $env:GITHUB_ENV
$ntos = Get-ChildItem wdk_pkg -Recurse -Filter "ntoskrnl.lib" |
Where-Object { $_.DirectoryName -like "*x64*" } |
Select-Object -First 1
if (!$ntos) { throw "ntoskrnl.lib not found in WDK NuGet package" }
$libRoot = Split-Path (Split-Path $ntos.DirectoryName -Parent) -Parent
Write-Host "WDK lib root: $libRoot"
echo "WDK_LIB_ROOT=$libRoot" >> $env:GITHUB_ENV
$specstr = Get-ChildItem wdk_pkg -Recurse -Filter "specstrings.h" |
Select-Object -First 1
if (!$specstr) { throw "specstrings.h not found in SDK NuGet package" }
$sdkIncRoot = Split-Path $specstr.DirectoryName -Parent
Write-Host "SDK include root: $sdkIncRoot"
echo "SDK_INC_ROOT=$sdkIncRoot" >> $env:GITHUB_ENV
- name: Build kernel driver
shell: cmd
run: call plugins\KernelMemory\driver\build_driver.bat
- name: Test
shell: bash
run: |
@@ -62,6 +92,7 @@ jobs:
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
mkdir -p release/Plugins
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
cp plugins/KernelMemory/driver/build/rcxdrv.sys release/Plugins/ 2>/dev/null || true
cp -r build/themes release/ 2>/dev/null || true
cp -r build/examples release/ 2>/dev/null || true
cp build/screenshot.png release/ 2>/dev/null || true

View File

@@ -531,6 +531,11 @@ if(BUILD_TESTING)
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
add_executable(test_mcp tests/test_mcp.cpp)
target_include_directories(test_mcp PRIVATE src)
target_link_libraries(test_mcp PRIVATE ${QT}::Core ${QT}::Network ${QT}::Test)
add_test(NAME test_mcp COMMAND test_mcp)
if(WIN32)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
@@ -539,6 +544,17 @@ if(BUILD_TESTING)
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)
add_executable(test_kernel_provider tests/test_kernel_provider.cpp
plugins/KernelMemory/KernelMemoryPlugin.cpp
src/processpicker.cpp src/processpicker.ui
src/scanner.cpp)
target_include_directories(test_kernel_provider PRIVATE
src plugins/KernelMemory)
target_link_libraries(test_kernel_provider PRIVATE
${QT}::Widgets ${QT}::Concurrent ${QT}::Test
psapi shell32 advapi32 ${_QT_WINEXTRAS})
add_test(NAME test_kernel_provider COMMAND test_kernel_provider)
endif()
add_executable(bench_large_class tests/bench_large_class.cpp
@@ -582,6 +598,7 @@ if(NOT APPLE)
add_subdirectory(plugins/RemoteProcessMemory)
endif()
if(WIN32)
add_subdirectory(plugins/KernelMemory)
add_subdirectory(plugins/WinDbgMemory)
add_subdirectory(plugins/RcNetPluginCompatLayer)
endif()

View File

@@ -0,0 +1,63 @@
cmake_minimum_required(VERSION 3.20)
project(KernelMemoryPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC OFF) # run uic manually to avoid dupbuild with ProcessMemoryPlugin
# ─── Generate ui_processpicker.h in our own build dir ────────────────
set(_UI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui")
set(_UI_HDR "${CMAKE_CURRENT_BINARY_DIR}/ui_processpicker.h")
add_custom_command(
OUTPUT "${_UI_HDR}"
COMMAND ${QT}::uic -o "${_UI_HDR}" "${_UI_SRC}"
DEPENDS "${_UI_SRC}"
COMMENT "UIC processpicker.ui (KernelMemoryPlugin)"
VERBATIM
)
# ─── Plugin DLL ──────────────────────────────────────────────────────
set(PLUGIN_SOURCES
KernelMemoryPlugin.h
KernelMemoryPlugin.cpp
rcx_drv_protocol.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
"${_UI_HDR}"
)
add_library(KernelMemoryPlugin SHARED ${PLUGIN_SOURCES})
target_link_libraries(KernelMemoryPlugin PRIVATE
${QT}::Widgets
${_QT_WINEXTRAS}
)
if(WIN32)
target_link_libraries(KernelMemoryPlugin PRIVATE psapi shell32 advapi32)
endif()
if(UNIX AND NOT APPLE)
target_compile_options(KernelMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
target_include_directories(KernelMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h
)
set_target_properties(KernelMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)
install(TARGETS KernelMemoryPlugin
LIBRARY DESTINATION Plugins
RUNTIME DESTINATION Plugins
)

View File

@@ -0,0 +1,751 @@
#include "KernelMemoryPlugin.h"
#include "../../src/processpicker.h"
#include <QStyle>
#include <QApplication>
#include <QMenu>
#include <QMessageBox>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QLibrary>
#ifdef _WIN32
#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <shellapi.h>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QtWin>
#endif
#endif
// ─────────────────────────────────────────────────────────────────────────
// Helper: DeviceIoControl wrapper
// ─────────────────────────────────────────────────────────────────────────
#ifdef _WIN32
static bool ioctlCall(HANDLE h, DWORD code,
const void* in, DWORD inLen,
void* out, DWORD outLen,
DWORD* bytesReturned = nullptr)
{
DWORD br = 0;
BOOL ok = DeviceIoControl(h, code, const_cast<LPVOID>(in), inLen,
out, outLen, &br, nullptr);
if (bytesReturned) *bytesReturned = br;
return ok != FALSE;
}
#endif // _WIN32
// ─────────────────────────────────────────────────────────────────────────
// KernelProcessProvider
// ─────────────────────────────────────────────────────────────────────────
KernelProcessProvider::KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName)
: m_driverHandle(driverHandle)
, m_pid(pid)
, m_processName(processName)
{
if (m_driverHandle) {
queryPeb();
cacheModules();
}
}
bool KernelProcessProvider::read(uint64_t addr, void* buf, int len) const
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
if (len > RCX_DRV_MAX_VIRTUAL) len = RCX_DRV_MAX_VIRTUAL;
RcxDrvReadRequest req{};
req.pid = m_pid;
req.address = addr;
req.length = (uint32_t)len;
DWORD br = 0;
BOOL ok = DeviceIoControl((HANDLE)m_driverHandle,
IOCTL_RCX_READ_MEMORY,
&req, sizeof(req),
buf, (DWORD)len, &br, nullptr);
// Zero unread portion (partial copy)
if ((int)br < len)
memset((char*)buf + br, 0, len - br);
return ok || br > 0;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
int KernelProcessProvider::size() const
{
return m_driverHandle ? 0x10000 : 0;
}
bool KernelProcessProvider::write(uint64_t addr, const void* buf, int len)
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
if (len > RCX_DRV_MAX_VIRTUAL) return false;
// Build request: header + inline data
QByteArray packet(sizeof(RcxDrvWriteRequest) + len, Qt::Uninitialized);
auto* req = reinterpret_cast<RcxDrvWriteRequest*>(packet.data());
req->pid = m_pid;
req->_pad0 = 0;
req->address = addr;
req->length = (uint32_t)len;
req->_pad1 = 0;
memcpy(packet.data() + sizeof(RcxDrvWriteRequest), buf, len);
return ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_MEMORY,
packet.constData(), (DWORD)packet.size(),
nullptr, 0);
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
QString KernelProcessProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules) {
if (addr >= mod.base && addr < mod.base + mod.size) {
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {};
}
uint64_t KernelProcessProvider::symbolToAddress(const QString& name) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
QVector<rcx::MemoryRegion> KernelProcessProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
#ifdef _WIN32
if (!m_driverHandle) return regions;
RcxDrvQueryRegionsRequest req{};
req.pid = m_pid;
// Allocate generous output buffer for region entries
constexpr int kMaxEntries = 8192;
QByteArray outBuf(kMaxEntries * sizeof(RcxDrvRegionEntry), Qt::Uninitialized);
DWORD br = 0;
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_REGIONS,
&req, sizeof(req),
outBuf.data(), (DWORD)outBuf.size(), &br))
return regions;
int count = (int)(br / sizeof(RcxDrvRegionEntry));
auto* entries = reinterpret_cast<const RcxDrvRegionEntry*>(outBuf.constData());
for (int i = 0; i < count; ++i) {
const auto& e = entries[i];
// Only include committed, accessible regions
if (!(e.state & 0x1000)) continue; // MEM_COMMIT = 0x1000
uint32_t p = e.protect;
if (p & 0x01) continue; // PAGE_NOACCESS
if (p & 0x100) continue; // PAGE_GUARD
rcx::MemoryRegion region;
region.base = e.base;
region.size = e.size;
region.readable = true;
region.writable = (p & 0x04) || (p & 0x08) || (p & 0x40) || (p & 0x80);
region.executable = (p & 0x10) || (p & 0x20) || (p & 0x40) || (p & 0x80);
// Match module name
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);
}
#endif
return regions;
}
void KernelProcessProvider::queryPeb()
{
#ifdef _WIN32
RcxDrvQueryPebRequest req{};
req.pid = m_pid;
RcxDrvQueryPebResponse resp{};
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_PEB,
&req, sizeof(req), &resp, sizeof(resp))) {
m_peb = resp.pebAddress;
if (resp.pointerSize == 4)
m_pointerSize = 4;
}
#endif
}
QVector<rcx::Provider::ThreadInfo> KernelProcessProvider::tebs() const
{
QVector<ThreadInfo> result;
#ifdef _WIN32
if (!m_driverHandle) return result;
RcxDrvQueryTebsRequest req{};
req.pid = m_pid;
constexpr int kMaxThreads = 4096;
QByteArray outBuf(kMaxThreads * sizeof(RcxDrvTebEntry), Qt::Uninitialized);
DWORD br = 0;
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_TEBS,
&req, sizeof(req),
outBuf.data(), (DWORD)outBuf.size(), &br))
return result;
int count = (int)(br / sizeof(RcxDrvTebEntry));
auto* entries = reinterpret_cast<const RcxDrvTebEntry*>(outBuf.constData());
for (int i = 0; i < count; ++i)
result.append({entries[i].tebAddress, entries[i].threadId});
#endif
return result;
}
void KernelProcessProvider::cacheModules()
{
#ifdef _WIN32
if (!m_driverHandle) return;
RcxDrvQueryModulesRequest req{};
req.pid = m_pid;
constexpr int kMaxModules = 1024;
QByteArray outBuf(kMaxModules * sizeof(RcxDrvModuleEntry), Qt::Uninitialized);
DWORD br = 0;
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_MODULES,
&req, sizeof(req),
outBuf.data(), (DWORD)outBuf.size(), &br))
return;
int count = (int)(br / sizeof(RcxDrvModuleEntry));
auto* entries = reinterpret_cast<const RcxDrvModuleEntry*>(outBuf.constData());
m_modules.reserve(count);
for (int i = 0; i < count; ++i) {
QString modName = QString::fromUtf16(reinterpret_cast<const char16_t*>(entries[i].name));
if (i == 0)
m_base = entries[i].base;
m_modules.append({modName, entries[i].base, entries[i].size});
}
#endif
}
// ─────────────────────────────────────────────────────────────────────────
// KernelProcessProvider — paging / address translation
// ─────────────────────────────────────────────────────────────────────────
uint64_t KernelProcessProvider::getCr3() const
{
#ifdef _WIN32
if (m_cr3Cache) return m_cr3Cache;
if (!m_driverHandle) return 0;
RcxDrvReadCr3Request req{};
req.pid = m_pid;
RcxDrvReadCr3Response resp{};
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_CR3,
&req, sizeof(req), &resp, sizeof(resp))) {
m_cr3Cache = resp.cr3;
return m_cr3Cache;
}
#endif
return 0;
}
rcx::VtopResult KernelProcessProvider::translateAddress(uint64_t va) const
{
rcx::VtopResult result{};
#ifdef _WIN32
if (!m_driverHandle) return result;
RcxDrvVtopRequest req{};
req.pid = m_pid;
req.virtualAddress = va;
RcxDrvVtopResponse resp{};
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_VTOP,
&req, sizeof(req), &resp, sizeof(resp))) {
result.physical = resp.physicalAddress;
result.pml4e = resp.pml4e;
result.pdpte = resp.pdpte;
result.pde = resp.pde;
result.pte = resp.pte;
result.pageSize = resp.pageSize;
result.valid = resp.valid != 0;
}
#else
Q_UNUSED(va);
#endif
return result;
}
QVector<uint64_t> KernelProcessProvider::readPageTable(uint64_t physAddr, int startIdx, int count) const
{
QVector<uint64_t> entries;
#ifdef _WIN32
if (!m_driverHandle) return entries;
if (startIdx < 0 || startIdx >= 512) return entries;
if (count <= 0) return entries;
if (startIdx + count > 512) count = 512 - startIdx;
// Read the full 4KB page table via physical read
int byteOffset = startIdx * 8;
int byteLen = count * 8;
QByteArray buf(byteLen, 0);
RcxDrvPhysReadRequest req{};
req.physAddress = physAddr + byteOffset;
req.length = (uint32_t)byteLen;
req.width = 0; // memcpy mode
DWORD br = 0;
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_PHYS,
&req, sizeof(req), buf.data(), (DWORD)byteLen, &br)) {
entries.resize(count);
memcpy(entries.data(), buf.constData(), byteLen);
}
#else
Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count);
#endif
return entries;
}
// ─────────────────────────────────────────────────────────────────────────
// KernelPhysProvider
// ─────────────────────────────────────────────────────────────────────────
KernelPhysProvider::KernelPhysProvider(void* driverHandle, uint64_t baseAddr)
: m_driverHandle(driverHandle)
, m_baseAddr(baseAddr)
{
}
bool KernelPhysProvider::read(uint64_t addr, void* buf, int len) const
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
// Read in 4KB chunks (driver cap)
int offset = 0;
while (offset < len) {
int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL);
RcxDrvPhysReadRequest req{};
req.physAddress = addr + offset;
req.length = (uint32_t)chunk;
req.width = 0; // memcpy mode
DWORD br = 0;
BOOL ok = DeviceIoControl((HANDLE)m_driverHandle,
IOCTL_RCX_READ_PHYS,
&req, sizeof(req),
(char*)buf + offset, (DWORD)chunk, &br, nullptr);
if (!ok && br == 0) {
memset((char*)buf + offset, 0, len - offset);
return offset > 0;
}
if ((int)br < chunk)
memset((char*)buf + offset + br, 0, chunk - br);
offset += chunk;
}
return true;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
bool KernelPhysProvider::write(uint64_t addr, const void* buf, int len)
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
int offset = 0;
while (offset < len) {
int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL);
QByteArray packet(sizeof(RcxDrvPhysWriteRequest) + chunk, Qt::Uninitialized);
auto* req = reinterpret_cast<RcxDrvPhysWriteRequest*>(packet.data());
req->physAddress = addr + offset;
req->length = (uint32_t)chunk;
req->width = 0;
memcpy(packet.data() + sizeof(RcxDrvPhysWriteRequest), (const char*)buf + offset, chunk);
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_PHYS,
packet.constData(), (DWORD)packet.size(),
nullptr, 0))
return false;
offset += chunk;
}
return true;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
// ─────────────────────────────────────────────────────────────────────────
// KernelMemoryPlugin
// ─────────────────────────────────────────────────────────────────────────
KernelMemoryPlugin::KernelMemoryPlugin()
{
}
KernelMemoryPlugin::~KernelMemoryPlugin()
{
stopDriver();
}
QIcon KernelMemoryPlugin::Icon() const
{
return qApp->style()->standardIcon(QStyle::SP_DriveHDIcon);
}
bool KernelMemoryPlugin::canHandle(const QString& target) const
{
return target.startsWith(QStringLiteral("km:"))
|| target.startsWith(QStringLiteral("phys:"));
}
std::unique_ptr<rcx::Provider> KernelMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{
if (!ensureDriverLoaded(errorMsg))
return nullptr;
#ifdef _WIN32
if (target.startsWith(QStringLiteral("km:"))) {
// km:{pid}:{name}
QStringList parts = target.mid(3).split(':');
bool ok = false;
uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target: ") + target;
return nullptr;
}
QString name = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid);
auto prov = std::make_unique<KernelProcessProvider>((void*)m_driverHandle, pid, name);
if (!prov->isValid()) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to read process %1 (PID: %2) via kernel driver.")
.arg(name).arg(pid);
return nullptr;
}
return prov;
}
if (target.startsWith(QStringLiteral("phys:"))) {
// phys:{baseAddr}
bool ok = false;
uint64_t baseAddr = target.mid(5).toULongLong(&ok, 16);
if (!ok) baseAddr = 0;
return std::make_unique<KernelPhysProvider>((void*)m_driverHandle, baseAddr);
}
#endif
if (errorMsg) *errorMsg = QStringLiteral("Unknown target format: ") + target;
return nullptr;
}
uint64_t KernelMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
if (target.startsWith(QStringLiteral("phys:"))) {
bool ok = false;
uint64_t addr = target.mid(5).toULongLong(&ok, 16);
return ok ? addr : 0;
}
// For process mode, the provider discovers base via modules
return 0;
}
bool KernelMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
// Show process picker directly (physical memory is accessed via
// context menu "Browse Page Tables" / "Follow Physical Frame" on an
// attached kernel process).
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
QList<ProcessInfo> processes;
for (const auto& pinfo : pluginProcesses) {
ProcessInfo info;
info.pid = pinfo.pid;
info.name = pinfo.name;
info.path = pinfo.path;
info.icon = pinfo.icon;
info.is32Bit = pinfo.is32Bit;
processes.append(info);
}
ProcessPicker picker(processes, parent);
if (picker.exec() == QDialog::Accepted) {
uint32_t pid = picker.selectedProcessId();
QString name = picker.selectedProcessName();
*target = QStringLiteral("km:%1:%2").arg(pid).arg(name);
return true;
}
return false;
}
QVector<PluginProcessInfo> KernelMemoryPlugin::enumerateProcesses()
{
QVector<PluginProcessInfo> processes;
#ifdef _WIN32
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) return processes;
PROCESSENTRY32W entry;
entry.dwSize = sizeof(entry);
if (Process32FirstW(snapshot, &entry)) {
do {
PluginProcessInfo info;
info.pid = entry.th32ProcessID;
info.name = QString::fromWCharArray(entry.szExeFile);
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID);
if (hProcess) {
wchar_t path[MAX_PATH * 2];
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
if (QueryFullProcessImageNameW(hProcess, 0, path, &pathLen)) {
info.path = QString::fromWCharArray(path);
SHFILEINFOW sfi = {};
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
if (sfi.hIcon) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPixmap pixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon));
#else
QPixmap pixmap = QtWin::fromHICON(sfi.hIcon);
#endif
info.icon = QIcon(pixmap);
DestroyIcon(sfi.hIcon);
}
}
}
BOOL isWow64 = FALSE;
if (IsWow64Process(hProcess, &isWow64) && isWow64)
info.is32Bit = true;
CloseHandle(hProcess);
}
processes.append(info);
} while (Process32NextW(snapshot, &entry));
}
CloseHandle(snapshot);
#endif
return processes;
}
void KernelMemoryPlugin::populatePluginMenu(QMenu* menu)
{
if (!m_driverLoaded) return;
menu->addAction(QStringLiteral("Unload Kernel Driver"), [this]() { unloadDriver(); });
}
// ─────────────────────────────────────────────────────────────────────────
// Driver service management
// ─────────────────────────────────────────────────────────────────────────
QString KernelMemoryPlugin::driverPath() const
{
// Resolve rcxdrv.sys next to the plugin DLL
QString pluginDir = QCoreApplication::applicationDirPath() + QStringLiteral("/Plugins");
return pluginDir + QStringLiteral("/rcxdrv.sys");
}
bool KernelMemoryPlugin::ensureDriverLoaded(QString* errorMsg)
{
#ifdef _WIN32
// Already connected?
if (m_driverLoaded && m_driverHandle != INVALID_HANDLE_VALUE) {
RcxDrvPingResponse ping{};
if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping)))
return true;
// Handle went stale — close it and try to reconnect
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
m_driverLoaded = false;
}
// Show wait cursor (SCM + StartService can take seconds on first load)
struct WaitCursorGuard {
WaitCursorGuard() { QGuiApplication::setOverrideCursor(Qt::WaitCursor); }
~WaitCursorGuard() { QGuiApplication::restoreOverrideCursor(); }
} waitCursor;
// Fast path: driver may already be running (previous session, or after disconnect).
// Just try to open the device handle directly.
m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH,
GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (m_driverHandle != INVALID_HANDLE_VALUE) {
RcxDrvPingResponse ping{};
if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) {
m_driverLoaded = true;
return true;
}
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
}
// Slow path: need to install/start the service.
QString sysPath = driverPath();
if (!QFileInfo::exists(sysPath)) {
if (errorMsg)
*errorMsg = QStringLiteral("Driver not found: %1\n\n"
"Place rcxdrv.sys in the Plugins folder next to the plugin DLL.").arg(sysPath);
return false;
}
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (!scm) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to open Service Control Manager.\n"
"Run Reclass as Administrator to load the kernel driver.");
return false;
}
// Try to open existing service first
SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS);
if (!svc) {
// Service doesn't exist — create it
std::wstring wPath = sysPath.toStdWString();
svc = CreateServiceW(scm, L"RcxDrv", L"RcxDrv",
SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
wPath.c_str(),
nullptr, nullptr, nullptr, nullptr, nullptr);
if (!svc) {
DWORD err = GetLastError();
if (errorMsg)
*errorMsg = QStringLiteral("Failed to create driver service (error %1).\n"
"Ensure test signing is enabled: bcdedit /set testsigning on").arg(err);
CloseServiceHandle(scm);
return false;
}
}
// Start service (ERROR_SERVICE_ALREADY_RUNNING is fine — means it's already up)
if (!StartServiceW(svc, 0, nullptr)) {
DWORD err = GetLastError();
if (err != ERROR_SERVICE_ALREADY_RUNNING) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to start driver (error %1).\n"
"Ensure test signing is enabled and the driver is properly signed.").arg(err);
CloseServiceHandle(svc);
CloseServiceHandle(scm);
return false;
}
}
// Done with SCM — don't hold handles open
CloseServiceHandle(svc);
CloseServiceHandle(scm);
// Open device handle
m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH,
GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (m_driverHandle == INVALID_HANDLE_VALUE) {
if (errorMsg)
*errorMsg = QStringLiteral("Driver started but could not open device handle.\n"
"Device path: %1").arg(QString::fromLatin1(RCX_DRV_USERMODE_PATH));
return false;
}
// Verify with ping
RcxDrvPingResponse ping{};
if (!ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) {
if (errorMsg)
*errorMsg = QStringLiteral("Driver opened but ping failed.");
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
return false;
}
m_driverLoaded = true;
return true;
#else
if (errorMsg)
*errorMsg = QStringLiteral("Kernel driver is only supported on Windows.");
return false;
#endif
}
void KernelMemoryPlugin::unloadDriver()
{
#ifdef _WIN32
// Close device handle only — service stays running so we can reconnect
if (m_driverHandle != INVALID_HANDLE_VALUE) {
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
}
m_driverLoaded = false;
#endif
}
void KernelMemoryPlugin::stopDriver()
{
#ifdef _WIN32
unloadDriver();
// Full cleanup: stop + delete the service
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (scm) {
SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS);
if (svc) {
SERVICE_STATUS ss;
ControlService(svc, SERVICE_CONTROL_STOP, &ss);
DeleteService(svc);
CloseServiceHandle(svc);
}
CloseServiceHandle(scm);
}
#endif
}
// ─────────────────────────────────────────────────────────────────────────
// Plugin factory
// ─────────────────────────────────────────────────────────────────────────
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new KernelMemoryPlugin();
}

View File

@@ -0,0 +1,142 @@
#pragma once
#include "../../src/iplugin.h"
#include "../../src/core.h"
#include "rcx_drv_protocol.h"
#include <cstdint>
#ifdef _WIN32
#include <windows.h>
#endif
// ─────────────────────────────────────────────────────────────────────────
// Provider variants
// ─────────────────────────────────────────────────────────────────────────
/**
* Kernel-mode process memory provider.
* Reads/writes target process virtual memory via IOCTL_RCX_READ/WRITE_MEMORY.
*/
class KernelProcessProvider : public rcx::Provider
{
public:
KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName);
~KernelProcessProvider() override = default;
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override;
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return true; }
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("KernelProcess"); }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
QVector<rcx::MemoryRegion> enumerateRegions() const override;
bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; }
uint32_t pid() const { return m_pid; }
uint64_t peb() const override { return m_peb; }
QVector<ThreadInfo> tebs() const override;
// ── Paging / address translation ──
bool hasKernelPaging() const override { return true; }
uint64_t getCr3() const override;
rcx::VtopResult translateAddress(uint64_t va) const override;
QVector<uint64_t> readPageTable(uint64_t physAddr, int startIdx = 0, int count = 512) const override;
void* driverHandle() const { return m_driverHandle; }
private:
void queryPeb();
void cacheModules();
void* m_driverHandle;
uint32_t m_pid;
QString m_processName;
uint64_t m_base = 0;
int m_pointerSize = 8;
uint64_t m_peb = 0;
mutable uint64_t m_cr3Cache = 0;
struct ModuleInfo {
QString name;
uint64_t base;
uint64_t size;
};
QVector<ModuleInfo> m_modules;
};
/**
* Kernel-mode physical memory provider.
* Reads/writes raw physical addresses via IOCTL_RCX_READ/WRITE_PHYS.
*/
class KernelPhysProvider : public rcx::Provider
{
public:
KernelPhysProvider(void* driverHandle, uint64_t baseAddr);
~KernelPhysProvider() override = default;
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override { return m_driverHandle ? 0x10000 : 0; }
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return true; }
QString name() const override { return QStringLiteral("Physical Memory"); }
QString kind() const override { return QStringLiteral("Physical"); }
bool isLive() const override { return true; }
uint64_t base() const override { return m_baseAddr; }
bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; }
void setBaseAddr(uint64_t addr) { m_baseAddr = addr; }
void* driverHandle() const { return m_driverHandle; }
private:
void* m_driverHandle;
uint64_t m_baseAddr;
};
// ─────────────────────────────────────────────────────────────────────────
// Plugin
// ─────────────────────────────────────────────────────────────────────────
class KernelMemoryPlugin : public IProviderPlugin
{
public:
KernelMemoryPlugin();
~KernelMemoryPlugin() override;
std::string Name() const override { return "Kernel Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override { return "Read and write memory via kernel driver (IOCTL)"; }
k_ELoadType LoadType() const override { return k_ELoadTypeManual; }
QIcon Icon() const override;
bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override;
bool providesProcessList() const override { return true; }
QVector<PluginProcessInfo> enumerateProcesses() override;
void populatePluginMenu(QMenu* menu) override;
private:
bool ensureDriverLoaded(QString* errorMsg = nullptr);
void unloadDriver(); // close handle only — service stays running
void stopDriver(); // full cleanup: close handle + stop + delete service
QString driverPath() const;
#ifdef _WIN32
HANDLE m_driverHandle = INVALID_HANDLE_VALUE;
#endif
bool m_driverLoaded = false;
};
// Plugin export
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

View File

@@ -0,0 +1,99 @@
@echo off
setlocal enabledelayedexpansion
:: ── Auto-detect MSVC (override with MSVC env var) ──
if not defined MSVC (
set "VSBASE=C:\Program Files\Microsoft Visual Studio\2022"
for %%E in (Enterprise Professional Community BuildTools) do (
if exist "!VSBASE!\%%E\VC\Tools\MSVC" (
for /f "delims=" %%V in ('dir /b /ad /o-n "!VSBASE!\%%E\VC\Tools\MSVC" 2^>nul') do (
if not defined MSVC set "MSVC=!VSBASE!\%%E\VC\Tools\MSVC\%%V"
)
)
)
)
if not defined MSVC (
echo ERROR: Could not find MSVC toolchain
exit /b 1
)
:: ── Auto-detect WDK (override with WDK_INC_ROOT and WDK_LIB_ROOT env vars) ──
:: SDK_INC_ROOT is optional; when WDK is installed traditionally, SDK shared
:: headers live alongside WDK headers. NuGet splits them into a separate package.
if not defined WDK_INC_ROOT (
set "WDK=C:\Program Files (x86)\Windows Kits\10"
set WDKVER=
for /f "delims=" %%V in ('dir /b /ad /o-n "!WDK!\Include" 2^>nul') do (
if exist "!WDK!\Include\%%V\km\ntddk.h" (
if not defined WDKVER set "WDKVER=%%V"
)
)
if not defined WDKVER (
echo ERROR: Could not find WDK headers under !WDK!\Include
echo Set WDK_INC_ROOT and WDK_LIB_ROOT environment variables to override.
exit /b 1
)
set "WDK_INC_ROOT=!WDK!\Include\!WDKVER!"
set "WDK_LIB_ROOT=!WDK!\Lib\!WDKVER!"
set "SDK_INC_ROOT=!WDK!\Include\!WDKVER!"
)
:: If SDK_INC_ROOT not set, default to WDK_INC_ROOT (traditional install has both)
if not defined SDK_INC_ROOT set "SDK_INC_ROOT=%WDK_INC_ROOT%"
echo Using MSVC: %MSVC%
echo Using WDK inc: %WDK_INC_ROOT%
echo Using SDK inc: %SDK_INC_ROOT%
echo Using WDK lib: %WDK_LIB_ROOT%
set "CL_EXE=%MSVC%\bin\Hostx64\x64\cl.exe"
set "LINK_EXE=%MSVC%\bin\Hostx64\x64\link.exe"
set "SRCDIR=%~dp0"
set "OUTDIR=%SRCDIR%build"
if not exist "%OUTDIR%" mkdir "%OUTDIR%"
echo === Compiling rcxdrv.c ===
"%CL_EXE%" /nologo /c /Zi /W4 /WX- /O2 /GS- ^
/D "NDEBUG" /D "_AMD64_" /D "AMD64" /D "_WIN64" /D "KERNEL" ^
/D "NTDDI_VERSION=0x0A000000" ^
/I "%WDK_INC_ROOT%\km" ^
/I "%WDK_INC_ROOT%\km\crt" ^
/I "%WDK_INC_ROOT%\shared" ^
/I "%SDK_INC_ROOT%\shared" ^
/I "%SDK_INC_ROOT%\ucrt" ^
/kernel ^
/Fo"%OUTDIR%\rcxdrv.obj" ^
"%SRCDIR%rcxdrv.c"
if errorlevel 1 goto :fail
echo === Linking rcxdrv.sys ===
"%LINK_EXE%" /nologo ^
/OUT:"%OUTDIR%\rcxdrv.sys" ^
/DRIVER:WDM ^
/SUBSYSTEM:NATIVE ^
/ENTRY:DriverEntry ^
/MACHINE:X64 ^
/NODEFAULTLIB ^
/RELEASE ^
/MERGE:.rdata=.text ^
/INTEGRITYCHECK ^
/PDBALTPATH:rcxdrv.pdb ^
/PDB:"%OUTDIR%\rcxdrv.pdb" ^
"%OUTDIR%\rcxdrv.obj" ^
"%WDK_LIB_ROOT%\km\x64\ntoskrnl.lib" ^
"%WDK_LIB_ROOT%\km\x64\hal.lib" ^
"%WDK_LIB_ROOT%\km\x64\BufferOverflowK.lib" ^
"%MSVC%\lib\x64\libcmt.lib"
if errorlevel 1 goto :fail
echo.
echo === SUCCESS ===
echo Output: %OUTDIR%\rcxdrv.sys
goto :eof
:fail
echo.
echo === BUILD FAILED ===
exit /b 1

View File

@@ -0,0 +1,808 @@
/*
* rcxdrv.c -- Minimal kernel-mode memory driver for Reclass.
*
* Provides: virtual memory R/W (per-process), physical memory R/W,
* region/PEB/module/TEB query, CR3 read, virtual-to-physical translation.
*
* Safety: all inputs validated, SEH around privileged instructions,
* MmCopyVirtualMemory for cross-process reads (no attach deadlock),
* METHOD_BUFFERED (no raw user pointers).
*/
#include <ntifs.h>
#include "../rcx_drv_protocol.h"
/* ── Undocumented but stable kernel exports (Vista+) ────────────── */
NTSTATUS NTAPI MmCopyVirtualMemory(
PEPROCESS SourceProcess, PVOID SourceAddress,
PEPROCESS TargetProcess, PVOID TargetAddress,
SIZE_T BufferSize, KPROCESSOR_MODE PreviousMode,
PSIZE_T ReturnSize);
PPEB NTAPI PsGetProcessPeb(PEPROCESS Process);
PVOID NTAPI PsGetProcessWow64Process(PEPROCESS Process);
PVOID NTAPI PsGetThreadTeb(PETHREAD Thread);
/*
* PsGetNextProcessThread is undocumented (not in any .lib).
* We resolve it dynamically via MmGetSystemRoutineAddress.
*/
typedef PETHREAD (NTAPI *PsGetNextProcessThread_t)(PEPROCESS Process, PETHREAD Thread);
static PsGetNextProcessThread_t g_PsGetNextProcessThread = NULL;
/* ── Manual structure definitions (kernel-mode) ─────────────────── */
/* These are partially opaque in WDK headers; define just the offsets we need. */
typedef struct _MEMORY_BASIC_INFORMATION_KM {
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
SIZE_T RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION_KM;
#define MEM_COMMIT_KM 0x1000
/* PEB.Ldr minimal definition for module enumeration */
typedef struct _PEB_LDR_DATA_KM {
UCHAR Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InLoadOrderModuleList;
} PEB_LDR_DATA_KM;
/* PEB minimal: only need Ldr at offset 0x18 (x64) */
typedef struct _PEB_KM {
UCHAR Reserved1[2];
UCHAR BeingDebugged;
UCHAR Reserved2[0x15];
PEB_LDR_DATA_KM* Ldr; /* offset 0x18 on x64 */
} PEB_KM;
/* LDR_DATA_TABLE_ENTRY minimal for walking InLoadOrderModuleList */
typedef struct _LDR_DATA_TABLE_ENTRY_KM {
LIST_ENTRY InLoadOrderLinks; /* offset 0x00 */
LIST_ENTRY InMemoryOrderLinks; /* offset 0x10 */
LIST_ENTRY InInitializationOrderLinks; /* offset 0x20 */
PVOID DllBase; /* offset 0x30 */
PVOID EntryPoint; /* offset 0x38 */
ULONG SizeOfImage; /* offset 0x40 */
ULONG _pad;
UNICODE_STRING FullDllName; /* offset 0x48 */
UNICODE_STRING BaseDllName; /* offset 0x58 */
} LDR_DATA_TABLE_ENTRY_KM;
/* ── Forward declarations ────────────────────────────────────────── */
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp);
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp);
DRIVER_UNLOAD DriverUnload;
/* ZwCurrentProcess() macro for ZwQueryVirtualMemory */
#ifndef ZwCurrentProcess
#define ZwCurrentProcess() ((HANDLE)(LONG_PTR)-1)
#endif
/* ── Helpers ─────────────────────────────────────────────────────── */
#define VALIDATE_INPUT(irp, stk, T) \
do { \
if ((stk)->Parameters.DeviceIoControl.InputBufferLength < sizeof(T)) { \
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
(irp)->IoStatus.Information = 0; \
IoCompleteRequest((irp), IO_NO_INCREMENT); \
return STATUS_BUFFER_TOO_SMALL; \
} \
} while (0)
#define VALIDATE_OUTPUT(irp, stk, minSize) \
do { \
if ((stk)->Parameters.DeviceIoControl.OutputBufferLength < (ULONG)(minSize)) { \
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
(irp)->IoStatus.Information = 0; \
IoCompleteRequest((irp), IO_NO_INCREMENT); \
return STATUS_BUFFER_TOO_SMALL; \
} \
} while (0)
static NTSTATUS LookupProcess(ULONG pid, PEPROCESS* proc)
{
return PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)pid, proc);
}
/* ── Safe physical mapping (MDL-based, avoids MmMapIoSpace BSOD) ── */
/*
* MmMapIoSpace/MmUnmapIoSpace BSODs (bugcheck 0x50 in
* MiClearMappingAndDereferenceIoSpace) when used on RAM-backed physical
* addresses. MDL-based mapping is safe for both RAM and MMIO.
*
* CRITICAL: cacheType must match the existing kernel mapping of the page.
* Use MmCached for RAM pages (already mapped cached by the kernel).
* Use MmNonCached ONLY for MMIO/device registers.
* Mismatched cache attributes (e.g. MmNonCached on RAM) cause silent
* kernel memory corruption via CPU cache coherency conflicts.
*/
typedef struct { PMDL mdl; PVOID base; } PHYS_MAP_CTX;
static PVOID MapPhysical(uint64_t physAddr, SIZE_T size,
MEMORY_CACHING_TYPE cacheType, PHYS_MAP_CTX* ctx)
{
ctx->mdl = NULL;
ctx->base = NULL;
ULONG_PTR pageOff = (ULONG_PTR)(physAddr & (PAGE_SIZE - 1));
SIZE_T totalSize = pageOff + size;
ULONG pages = (ULONG)((totalSize + PAGE_SIZE - 1) / PAGE_SIZE);
PMDL mdl = IoAllocateMdl(NULL, (ULONG)totalSize, FALSE, FALSE, NULL);
if (!mdl) return NULL;
PPFN_NUMBER pfn = MmGetMdlPfnArray(mdl);
PFN_NUMBER startPfn = (PFN_NUMBER)(physAddr / PAGE_SIZE);
for (ULONG i = 0; i < pages; i++)
pfn[i] = startPfn + i;
mdl->MdlFlags |= MDL_PAGES_LOCKED;
__try {
ctx->base = MmMapLockedPagesSpecifyCache(
mdl, KernelMode, cacheType, NULL, FALSE, NormalPagePriority);
} __except (EXCEPTION_EXECUTE_HANDLER) {
IoFreeMdl(mdl);
return NULL;
}
if (!ctx->base) { IoFreeMdl(mdl); return NULL; }
ctx->mdl = mdl;
return (PUCHAR)ctx->base + pageOff;
}
static void UnmapPhysical(PHYS_MAP_CTX* ctx)
{
if (ctx->base) MmUnmapLockedPages(ctx->base, ctx->mdl);
if (ctx->mdl) IoFreeMdl(ctx->mdl);
ctx->base = NULL;
ctx->mdl = NULL;
}
/* ── Virtual memory read ─────────────────────────────────────────── */
static NTSTATUS HandleReadMemory(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvReadRequest);
struct RcxDrvReadRequest* req = (struct RcxDrvReadRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
return STATUS_INVALID_PARAMETER;
VALIDATE_OUTPUT(irp, stk, req->length);
/* Save request fields before MmCopyVirtualMemory overwrites SystemBuffer.
* METHOD_BUFFERED aliases input and output to the same buffer, so the
* copy destination (SystemBuffer) clobbers req->* fields. */
ULONG pid = req->pid;
uint64_t address = req->address;
ULONG length = req->length;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(pid, &proc);
if (!NT_SUCCESS(st)) return st;
SIZE_T bytesRead = 0;
st = MmCopyVirtualMemory(
proc, (PVOID)address,
PsGetCurrentProcess(), irp->AssociatedIrp.SystemBuffer,
(SIZE_T)length, KernelMode, &bytesRead);
ObDereferenceObject(proc);
/* Partial reads: zero remainder, report success */
if (st == STATUS_PARTIAL_COPY) {
RtlZeroMemory((PUCHAR)irp->AssociatedIrp.SystemBuffer + bytesRead,
length - bytesRead);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
irp->IoStatus.Information = NT_SUCCESS(st) ? length : 0;
return st;
}
/* ── Virtual memory write ────────────────────────────────────────── */
static NTSTATUS HandleWriteMemory(PIRP irp, PIO_STACK_LOCATION stk)
{
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
if (inputLen < sizeof(struct RcxDrvWriteRequest))
return STATUS_BUFFER_TOO_SMALL;
struct RcxDrvWriteRequest* req = (struct RcxDrvWriteRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
return STATUS_INVALID_PARAMETER;
if (inputLen < sizeof(struct RcxDrvWriteRequest) + req->length)
return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
PUCHAR data = (PUCHAR)req + sizeof(struct RcxDrvWriteRequest);
SIZE_T bytesWritten = 0;
st = MmCopyVirtualMemory(
PsGetCurrentProcess(), data,
proc, (PVOID)req->address,
(SIZE_T)req->length, KernelMode, &bytesWritten);
ObDereferenceObject(proc);
irp->IoStatus.Information = 0;
return st;
}
/* ── Physical memory read ────────────────────────────────────────── */
static NTSTATUS HandleReadPhys(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvPhysReadRequest);
struct RcxDrvPhysReadRequest* req = (struct RcxDrvPhysReadRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
return STATUS_INVALID_PARAMETER;
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
return STATUS_INVALID_PARAMETER;
VALIDATE_OUTPUT(irp, stk, req->length);
/* Save request fields before SystemBuffer is overwritten (METHOD_BUFFERED
* aliases input and output to the same buffer). */
uint64_t physAddress = req->physAddress;
ULONG length = req->length;
ULONG width = req->width;
PUCHAR dst = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
if (width == 0) {
/* Byte copy -- use MmCopyMemory (safe for both RAM and MMIO) */
MM_COPY_ADDRESS srcAddr;
srcAddr.PhysicalAddress.QuadPart = (LONGLONG)physAddress;
SIZE_T bytesCopied = 0;
NTSTATUS st = MmCopyMemory(dst, srcAddr, (SIZE_T)length,
MM_COPY_MEMORY_PHYSICAL, &bytesCopied);
if (!NT_SUCCESS(st)) return st;
if (bytesCopied < length)
RtlZeroMemory(dst + bytesCopied, length - bytesCopied);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
/* Width-aware MMIO reads -- map via MDL (safe for all physical addresses).
* Use MmNonCached: width>0 implies MMIO register access where uncached
* semantics are required for correct device interaction. */
PHYS_MAP_CTX mapCtx;
PUCHAR src = (PUCHAR)MapPhysical(physAddress, (SIZE_T)length, MmNonCached, &mapCtx);
if (!src) return STATUS_UNSUCCESSFUL;
__try {
ULONG off = 0;
while (off + width <= length) {
if (width == 1)
dst[off] = READ_REGISTER_UCHAR(&src[off]);
else if (width == 2)
*(USHORT*)(dst + off) = READ_REGISTER_USHORT((PUSHORT)(src + off));
else
*(ULONG*)(dst + off) = READ_REGISTER_ULONG((PULONG)(src + off));
off += width;
}
if (off < length)
RtlZeroMemory(dst + off, length - off);
} __except (EXCEPTION_EXECUTE_HANDLER) {
UnmapPhysical(&mapCtx);
return STATUS_UNSUCCESSFUL;
}
UnmapPhysical(&mapCtx);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
/* ── Physical memory write ───────────────────────────────────────── */
static NTSTATUS HandleWritePhys(PIRP irp, PIO_STACK_LOCATION stk)
{
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest))
return STATUS_BUFFER_TOO_SMALL;
struct RcxDrvPhysWriteRequest* req = (struct RcxDrvPhysWriteRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
return STATUS_INVALID_PARAMETER;
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
return STATUS_INVALID_PARAMETER;
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest) + req->length)
return STATUS_BUFFER_TOO_SMALL;
PUCHAR src = (PUCHAR)req + sizeof(struct RcxDrvPhysWriteRequest);
/* Map via MDL (safe for both RAM and MMIO).
* width==0 → RAM byte write (MmCached to avoid cache attribute conflict).
* width>0 → MMIO register write (MmNonCached for correct device semantics). */
MEMORY_CACHING_TYPE ct = (req->width == 0) ? MmCached : MmNonCached;
PHYS_MAP_CTX mapCtx;
PUCHAR dst = (PUCHAR)MapPhysical(req->physAddress, (SIZE_T)req->length, ct, &mapCtx);
if (!dst) return STATUS_UNSUCCESSFUL;
__try {
if (req->width == 0) {
RtlCopyMemory(dst, src, req->length);
} else {
ULONG off = 0;
while (off + req->width <= req->length) {
if (req->width == 1)
WRITE_REGISTER_UCHAR(&dst[off], src[off]);
else if (req->width == 2)
WRITE_REGISTER_USHORT((PUSHORT)(dst + off), *(USHORT*)(src + off));
else
WRITE_REGISTER_ULONG((PULONG)(dst + off), *(ULONG*)(src + off));
off += req->width;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
UnmapPhysical(&mapCtx);
return STATUS_UNSUCCESSFUL;
}
UnmapPhysical(&mapCtx);
irp->IoStatus.Information = 0;
return STATUS_SUCCESS;
}
/* ── Ping ────────────────────────────────────────────────────────── */
static NTSTATUS HandlePing(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvPingResponse));
struct RcxDrvPingResponse* rsp = (struct RcxDrvPingResponse*)irp->AssociatedIrp.SystemBuffer;
rsp->version = RCX_DRV_VERSION;
rsp->driverBuild = __LINE__;
irp->IoStatus.Information = sizeof(struct RcxDrvPingResponse);
return STATUS_SUCCESS;
}
/* ── Query PEB ───────────────────────────────────────────────────── */
static NTSTATUS HandleQueryPeb(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryPebRequest);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvQueryPebResponse));
struct RcxDrvQueryPebRequest* req = (struct RcxDrvQueryPebRequest*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvQueryPebResponse* rsp = (struct RcxDrvQueryPebResponse*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
rsp->pebAddress = (uint64_t)(ULONG_PTR)PsGetProcessPeb(proc);
rsp->pointerSize = 8;
rsp->_pad = 0;
/* Detect WoW64 (32-bit process on 64-bit OS) */
PVOID wow64 = PsGetProcessWow64Process(proc);
if (wow64) {
rsp->pebAddress = (uint64_t)(ULONG_PTR)wow64;
rsp->pointerSize = 4;
}
ObDereferenceObject(proc);
irp->IoStatus.Information = sizeof(struct RcxDrvQueryPebResponse);
return STATUS_SUCCESS;
}
/* ── Query Regions ───────────────────────────────────────────────── */
static NTSTATUS HandleQueryRegions(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryRegionsRequest);
struct RcxDrvQueryRegionsRequest* req = (struct RcxDrvQueryRegionsRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvRegionEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Attach to target process to query its address space.
* IOCTLs arrive at PASSIVE_LEVEL; KeStackAttachProcess requires <= APC_LEVEL.
* ZwQueryVirtualMemory with ZwCurrentProcess() while attached queries the
* attached process's address space (correct). */
KAPC_STATE apcState;
KeStackAttachProcess(proc, &apcState);
struct RcxDrvRegionEntry* entries = (struct RcxDrvRegionEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
PVOID addr = NULL;
MEMORY_BASIC_INFORMATION_KM mbi;
while (count < maxEntries) {
SIZE_T retLen = 0;
st = ZwQueryVirtualMemory(ZwCurrentProcess(), addr, 0 /*MemoryBasicInformation*/,
&mbi, sizeof(mbi), &retLen);
if (!NT_SUCCESS(st)) break;
if (mbi.State == MEM_COMMIT_KM) {
entries[count].base = (uint64_t)(ULONG_PTR)mbi.BaseAddress;
entries[count].size = (uint64_t)mbi.RegionSize;
entries[count].protect = mbi.Protect;
entries[count].state = mbi.State;
count++;
}
ULONG_PTR next = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize;
if (next <= (ULONG_PTR)addr) break; /* overflow */
addr = (PVOID)next;
}
KeUnstackDetachProcess(&apcState);
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvRegionEntry);
return STATUS_SUCCESS;
}
/* ── Query Modules ───────────────────────────────────────────────── */
static NTSTATUS HandleQueryModules(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryModulesRequest);
struct RcxDrvQueryModulesRequest* req = (struct RcxDrvQueryModulesRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvModuleEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Attach to target process to read PEB->Ldr */
KAPC_STATE apcState;
KeStackAttachProcess(proc, &apcState);
struct RcxDrvModuleEntry* entries = (struct RcxDrvModuleEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
__try {
/* Read PEB address */
PEB_KM* peb = (PEB_KM*)PsGetProcessPeb(proc);
if (!peb) goto done;
ProbeForRead(peb, sizeof(PEB_KM), 1);
/* PEB->Ldr at offset 0x18 (x64) */
PEB_LDR_DATA_KM* ldr = peb->Ldr;
if (!ldr) goto done;
ProbeForRead(ldr, sizeof(PEB_LDR_DATA_KM), 1);
/* Walk InLoadOrderModuleList */
LIST_ENTRY* head = &ldr->InLoadOrderModuleList;
LIST_ENTRY* cur = head->Flink;
while (cur != head && count < maxEntries) {
LDR_DATA_TABLE_ENTRY_KM* entry = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY_KM, InLoadOrderLinks);
entries[count].base = (uint64_t)(ULONG_PTR)entry->DllBase;
entries[count].size = (uint64_t)entry->SizeOfImage;
/* Copy wide-char name (truncate to 259 chars + null) */
USHORT nameLen = entry->BaseDllName.Length / sizeof(WCHAR);
if (nameLen > 259) nameLen = 259;
if (entry->BaseDllName.Buffer) {
RtlCopyMemory(entries[count].name, entry->BaseDllName.Buffer,
nameLen * sizeof(uint16_t));
}
entries[count].name[nameLen] = 0;
count++;
cur = cur->Flink;
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
/* Partial results are fine */
}
done:
KeUnstackDetachProcess(&apcState);
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvModuleEntry);
return STATUS_SUCCESS;
}
/* ── Query TEBs ──────────────────────────────────────────────────── */
/*
* Walk the target process's thread list to collect TEB addresses.
* Uses PsGetNextProcessThread (undocumented but stable since Vista).
*/
static NTSTATUS HandleQueryTebs(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryTebsRequest);
struct RcxDrvQueryTebsRequest* req = (struct RcxDrvQueryTebsRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvTebEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
struct RcxDrvTebEntry* entries = (struct RcxDrvTebEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
if (!g_PsGetNextProcessThread) {
ObDereferenceObject(proc);
return STATUS_NOT_SUPPORTED;
}
/* PsGetNextProcessThread increments the ref on the returned PETHREAD and
* dereferences the previous one. We must release the last thread if we
* exit the loop early (exception or maxEntries hit). */
{
PETHREAD thread = NULL;
__try {
while ((thread = g_PsGetNextProcessThread(proc, thread)) != NULL) {
if (count >= maxEntries) {
/* Hit limit — release the thread PsGetNextProcessThread just returned */
ObDereferenceObject(thread);
break;
}
PVOID teb = PsGetThreadTeb(thread);
if (teb) {
entries[count].tebAddress = (uint64_t)(ULONG_PTR)teb;
entries[count].threadId = (uint32_t)(ULONG_PTR)PsGetThreadId(thread);
entries[count]._pad = 0;
count++;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
/* Exception mid-iteration: thread holds a referenced PETHREAD — release it */
if (thread)
ObDereferenceObject(thread);
}
}
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvTebEntry);
return STATUS_SUCCESS;
}
/* ── Read CR3 (DirectoryTableBase) ────────────────────────────────── */
/*
* EPROCESS.DirectoryTableBase offset. Stable across Win10/11 x64.
* Verified: 0x028 on 1507-22H2+ (KPROCESS is at offset 0 of EPROCESS).
*/
#define KPROCESS_DIRECTORY_TABLE_BASE 0x028
static NTSTATUS HandleReadCr3(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvReadCr3Request);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvReadCr3Response));
struct RcxDrvReadCr3Request* req = (struct RcxDrvReadCr3Request*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvReadCr3Response* rsp = (struct RcxDrvReadCr3Response*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
__try {
rsp->cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
/* Mask off PCID bits (bits 0-11) to get the PML4 physical address */
rsp->cr3 &= ~0xFFFULL;
rsp->kernelCr3 = rsp->cr3; /* same on non-KPTI; KPTI shadow is not easily accessible */
} __except (EXCEPTION_EXECUTE_HANDLER) {
ObDereferenceObject(proc);
return STATUS_UNSUCCESSFUL;
}
ObDereferenceObject(proc);
irp->IoStatus.Information = sizeof(struct RcxDrvReadCr3Response);
return STATUS_SUCCESS;
}
/* ── Virtual-to-Physical address translation ─────────────────────── */
/* NOTE: This walks the page table non-atomically via 4 sequential physical reads.
* The page table can be modified between reads (e.g., page-out, remap). This is
* an inherent limitation shared by WinDbg's !vtop and similar tools. For a
* debugging/reversing tool this tradeoff is acceptable. */
/* Extract physical frame address from a page table entry (bits 51:12) */
#define PTE_FRAME(pte) ((pte) & 0x000FFFFFFFFFF000ULL)
/* Check Present bit (bit 0) */
#define PTE_PRESENT(pte) ((pte) & 1ULL)
/* Check Page Size bit (bit 7) -- indicates large/huge page */
#define PTE_PS(pte) ((pte) & (1ULL << 7))
static NTSTATUS HandleVtop(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvVtopRequest);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvVtopResponse));
struct RcxDrvVtopRequest* req = (struct RcxDrvVtopRequest*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvVtopResponse* rsp = (struct RcxDrvVtopResponse*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Read CR3 */
uint64_t cr3;
__try {
cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
cr3 &= ~0xFFFULL;
} __except (EXCEPTION_EXECUTE_HANDLER) {
ObDereferenceObject(proc);
return STATUS_UNSUCCESSFUL;
}
ObDereferenceObject(proc);
uint64_t va = req->virtualAddress;
RtlZeroMemory(rsp, sizeof(*rsp));
/* Extract indices from virtual address:
* [47:39] = PML4 index, [38:30] = PDPT index,
* [29:21] = PD index, [20:12] = PT index,
* [11:0] = page offset */
ULONG pml4Idx = (ULONG)((va >> 39) & 0x1FF);
ULONG pdptIdx = (ULONG)((va >> 30) & 0x1FF);
ULONG pdIdx = (ULONG)((va >> 21) & 0x1FF);
ULONG ptIdx = (ULONG)((va >> 12) & 0x1FF);
MM_COPY_ADDRESS ca;
SIZE_T copied;
uint64_t entry;
/* Level 4: PML4 -- use MmCopyMemory (safe for RAM, unlike MmMapIoSpace) */
ca.PhysicalAddress.QuadPart = (LONGLONG)(cr3 + pml4Idx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pml4e = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
/* Level 3: PDPT */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdptIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pdpte = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
if (PTE_PS(entry)) {
/* 1GB huge page: physical = frame[51:30] | va[29:0] */
rsp->physicalAddress = (entry & 0x000FFFFFC0000000ULL) | (va & 0x3FFFFFFFULL);
rsp->pageSize = 2;
rsp->valid = 1;
goto done;
}
/* Level 2: PD */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pde = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
if (PTE_PS(entry)) {
/* 2MB large page: physical = frame[51:21] | va[20:0] */
rsp->physicalAddress = (entry & 0x000FFFFFFFE00000ULL) | (va & 0x1FFFFFULL);
rsp->pageSize = 1;
rsp->valid = 1;
goto done;
}
/* Level 1: PT */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + ptIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pte = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
/* 4KB page: physical = frame[51:12] | va[11:0] */
rsp->physicalAddress = PTE_FRAME(entry) | (va & 0xFFFULL);
rsp->pageSize = 0;
rsp->valid = 1;
done:
irp->IoStatus.Information = sizeof(struct RcxDrvVtopResponse);
return STATUS_SUCCESS;
}
/* ── IOCTL dispatch ──────────────────────────────────────────────── */
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp)
{
UNREFERENCED_PARAMETER(dev);
PIO_STACK_LOCATION stk = IoGetCurrentIrpStackLocation(irp);
NTSTATUS st;
switch (stk->Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_RCX_READ_MEMORY: st = HandleReadMemory(irp, stk); break;
case IOCTL_RCX_WRITE_MEMORY: st = HandleWriteMemory(irp, stk); break;
case IOCTL_RCX_QUERY_REGIONS: st = HandleQueryRegions(irp, stk); break;
case IOCTL_RCX_QUERY_PEB: st = HandleQueryPeb(irp, stk); break;
case IOCTL_RCX_QUERY_MODULES: st = HandleQueryModules(irp, stk); break;
case IOCTL_RCX_QUERY_TEBS: st = HandleQueryTebs(irp, stk); break;
case IOCTL_RCX_PING: st = HandlePing(irp, stk); break;
case IOCTL_RCX_READ_PHYS: st = HandleReadPhys(irp, stk); break;
case IOCTL_RCX_WRITE_PHYS: st = HandleWritePhys(irp, stk); break;
case IOCTL_RCX_READ_CR3: st = HandleReadCr3(irp, stk); break;
case IOCTL_RCX_VTOP: st = HandleVtop(irp, stk); break;
default:
st = STATUS_INVALID_DEVICE_REQUEST;
irp->IoStatus.Information = 0;
break;
}
irp->IoStatus.Status = st;
IoCompleteRequest(irp, IO_NO_INCREMENT);
return st;
}
/* ── Create / Close (permit open/close) ──────────────────────────── */
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp)
{
UNREFERENCED_PARAMETER(dev);
irp->IoStatus.Status = STATUS_SUCCESS;
irp->IoStatus.Information = 0;
IoCompleteRequest(irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
/* ── Unload ──────────────────────────────────────────────────────── */
void DriverUnload(PDRIVER_OBJECT drv)
{
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
IoDeleteSymbolicLink(&symlink);
if (drv->DeviceObject)
IoDeleteDevice(drv->DeviceObject);
}
/* ── Entry point ─────────────────────────────────────────────────── */
NTSTATUS DriverEntry(PDRIVER_OBJECT drv, PUNICODE_STRING regPath)
{
UNREFERENCED_PARAMETER(regPath);
/* Resolve undocumented APIs */
UNICODE_STRING fnName = RTL_CONSTANT_STRING(L"PsGetNextProcessThread");
g_PsGetNextProcessThread = (PsGetNextProcessThread_t)MmGetSystemRoutineAddress(&fnName);
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\RcxDrv");
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
PDEVICE_OBJECT devObj = NULL;
NTSTATUS st = IoCreateDevice(drv, 0, &devName, FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN, FALSE, &devObj);
if (!NT_SUCCESS(st)) return st;
st = IoCreateSymbolicLink(&symlink, &devName);
if (!NT_SUCCESS(st)) {
IoDeleteDevice(devObj);
return st;
}
drv->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
drv->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
drv->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;
drv->DriverUnload = DriverUnload;
devObj->Flags |= DO_BUFFERED_IO;
devObj->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}

View File

@@ -0,0 +1,17 @@
obj-m += rcxkm.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
install:
insmod rcxkm.ko
uninstall:
rmmod rcxkm
.PHONY: all clean install uninstall

View File

@@ -0,0 +1,132 @@
/*
* rcxkm.c -- Linux kernel module stub for Reclass kernel memory provider.
*
* Provides /dev/rcxkm char device with ioctl() dispatch using the same
* protocol structs as the Windows driver (rcx_drv_protocol.h).
*
* Build: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
*
* TODO: implement handlers (currently returns -ENOSYS for all IOCTLs).
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/mm.h>
#include "../rcx_drv_protocol.h"
#define DEVICE_NAME "rcxkm"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Reclass");
MODULE_DESCRIPTION("Reclass kernel memory provider (stub)");
/* ── IOCTL dispatch ─────────────────────────────────────────────────── */
static long rcxkm_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
(void)filp;
(void)arg;
switch (cmd) {
case IOCTL_RCX_READ_MEMORY:
/* TODO: find_get_pid(pid) -> get_task_struct -> access_process_vm() */
return -ENOSYS;
case IOCTL_RCX_WRITE_MEMORY:
/* TODO: access_process_vm() with FOLL_WRITE */
return -ENOSYS;
case IOCTL_RCX_QUERY_REGIONS:
/* TODO: walk target mm->mmap via VMA iteration */
return -ENOSYS;
case IOCTL_RCX_QUERY_PEB:
/* N/A on Linux (no PEB); could return mm->start_brk or similar */
return -ENOSYS;
case IOCTL_RCX_QUERY_MODULES:
/* TODO: walk target /proc/pid/maps or mm VMAs */
return -ENOSYS;
case IOCTL_RCX_QUERY_TEBS:
/* N/A on Linux (no TEB) */
return -ENOSYS;
case IOCTL_RCX_PING: {
struct RcxDrvPingResponse resp = {
.version = RCX_DRV_VERSION,
.driverBuild = 1,
};
if (copy_to_user((void __user *)arg, &resp, sizeof(resp)))
return -EFAULT;
return 0;
}
case IOCTL_RCX_READ_PHYS:
/* TODO: ioremap() + memcpy_fromio() */
return -ENOSYS;
case IOCTL_RCX_WRITE_PHYS:
/* TODO: ioremap() + memcpy_toio() */
return -ENOSYS;
default:
return -EINVAL;
}
}
/* ── File operations ────────────────────────────────────────────────── */
static int rcxkm_open(struct inode *inode, struct file *filp)
{
(void)inode; (void)filp;
return 0;
}
static int rcxkm_release(struct inode *inode, struct file *filp)
{
(void)inode; (void)filp;
return 0;
}
static const struct file_operations rcxkm_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = rcxkm_ioctl,
.open = rcxkm_open,
.release = rcxkm_release,
};
static struct miscdevice rcxkm_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &rcxkm_fops,
};
/* ── Module init/exit ───────────────────────────────────────────────── */
static int __init rcxkm_init(void)
{
int ret = misc_register(&rcxkm_device);
if (ret) {
pr_err("rcxkm: failed to register misc device (err=%d)\n", ret);
return ret;
}
pr_info("rcxkm: loaded, device /dev/%s\n", DEVICE_NAME);
return 0;
}
static void __exit rcxkm_exit(void)
{
misc_deregister(&rcxkm_device);
pr_info("rcxkm: unloaded\n");
}
module_init(rcxkm_init);
module_exit(rcxkm_exit);

View File

@@ -0,0 +1,189 @@
/*
* RCX Driver Protocol -- shared between kernel driver and usermode plugin.
* No dependencies beyond standard C headers. Pure C, no Windows types.
*/
#pragma once
#ifdef KERNEL
/* Kernel mode build: avoid stdint.h (not in WDK km/crt) */
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __int64 uint64_t;
typedef signed __int64 int64_t;
#else
#include <stdint.h>
#endif
/* ── Device / service names ───────────────────────────────────────── */
#define RCX_DRV_DEVICE_NAME L"\\Device\\RcxDrv"
#define RCX_DRV_SYMLINK_NAME L"\\DosDevices\\RcxDrv"
#define RCX_DRV_USERMODE_PATH "\\\\.\\RcxDrv"
#define RCX_DRV_SERVICE_NAME "RcxDrv"
/* ── Protocol version ─────────────────────────────────────────────── */
#define RCX_DRV_VERSION 1
/* ── Size limits ──────────────────────────────────────────────────── */
#define RCX_DRV_MAX_VIRTUAL (1024 * 1024) /* 1 MB per virtual read/write */
#define RCX_DRV_MAX_PHYSICAL 4096 /* 4 KB per physical read/write */
/* ── IOCTL codes ──────────────────────────────────────────────────── */
/* CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, function, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0) */
/* Virtual memory (per-process) */
#define IOCTL_RCX_READ_MEMORY 0x222000 /* function 0x800 */
#define IOCTL_RCX_WRITE_MEMORY 0x222004 /* function 0x801 */
#define IOCTL_RCX_QUERY_REGIONS 0x222008 /* function 0x802 */
#define IOCTL_RCX_QUERY_PEB 0x22200C /* function 0x803 */
#define IOCTL_RCX_QUERY_MODULES 0x222010 /* function 0x804 */
#define IOCTL_RCX_QUERY_TEBS 0x222014 /* function 0x805 */
#define IOCTL_RCX_PING 0x222018 /* function 0x806 */
/* Physical memory (MMIO) */
#define IOCTL_RCX_READ_PHYS 0x22201C /* function 0x807 */
#define IOCTL_RCX_WRITE_PHYS 0x222020 /* function 0x808 */
/* Paging / address translation */
#define IOCTL_RCX_READ_CR3 0x222044 /* function 0x811 */
#define IOCTL_RCX_VTOP 0x222048 /* function 0x812 */
/* ── Request / Response structures ────────────────────────────────── */
/* All structs are naturally aligned. Padding fields are explicit. */
/* -- Virtual memory -- */
struct RcxDrvReadRequest {
uint32_t pid;
uint32_t _pad0;
uint64_t address;
uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */
uint32_t _pad1;
};
/* Write: input = header + inline data bytes */
struct RcxDrvWriteRequest {
uint32_t pid;
uint32_t _pad0;
uint64_t address;
uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */
uint32_t _pad1;
/* uint8_t data[length] follows */
};
/* -- Region enumeration -- */
struct RcxDrvQueryRegionsRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvRegionEntry {
uint64_t base;
uint64_t size;
uint32_t protect; /* raw PAGE_* flags */
uint32_t state; /* MEM_COMMIT etc. */
};
/* -- PEB -- */
struct RcxDrvQueryPebRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvQueryPebResponse {
uint64_t pebAddress;
uint32_t pointerSize; /* 4 or 8 */
uint32_t _pad;
};
/* -- Modules -- */
struct RcxDrvQueryModulesRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvModuleEntry {
uint64_t base;
uint64_t size;
uint16_t name[260]; /* wide-char, null-terminated */
};
/* -- TEBs -- */
struct RcxDrvQueryTebsRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvTebEntry {
uint64_t tebAddress;
uint32_t threadId;
uint32_t _pad;
};
/* -- Ping -- */
struct RcxDrvPingResponse {
uint32_t version;
uint32_t driverBuild;
};
/* -- Physical memory -- */
struct RcxDrvPhysReadRequest {
uint64_t physAddress;
uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */
uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */
};
struct RcxDrvPhysWriteRequest {
uint64_t physAddress;
uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */
uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */
/* uint8_t data[length] follows */
};
/* -- Paging / address translation -- */
struct RcxDrvReadCr3Request {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvReadCr3Response {
uint64_t cr3; /* DirectoryTableBase (PML4 physical address) */
uint64_t kernelCr3; /* KernelDirectoryTableBase (KPTI shadow) */
};
struct RcxDrvVtopRequest {
uint32_t pid;
uint32_t _pad;
uint64_t virtualAddress;
};
struct RcxDrvVtopResponse {
uint64_t physicalAddress; /* final translated physical address (with page offset) */
uint64_t pml4e; /* raw PML4 entry value */
uint64_t pdpte; /* raw PDPT entry value */
uint64_t pde; /* raw PD entry value */
uint64_t pte; /* raw PT entry value (0 if large/huge page) */
uint8_t pageSize; /* 0=4KB, 1=2MB, 2=1GB */
uint8_t valid; /* 1 if translation succeeded, 0 if not present */
uint8_t _pad2[6];
};
/* ── Compile-time validation ──────────────────────────────────────── */
#ifdef __cplusplus
static_assert(sizeof(RcxDrvReadRequest) == 24, "ReadRequest layout");
static_assert(sizeof(RcxDrvWriteRequest) == 24, "WriteRequest layout");
static_assert(sizeof(RcxDrvRegionEntry) == 24, "RegionEntry layout");
static_assert(sizeof(RcxDrvModuleEntry) == 536, "ModuleEntry layout");
static_assert(sizeof(RcxDrvTebEntry) == 16, "TebEntry layout");
static_assert(sizeof(RcxDrvPingResponse) == 8, "PingResponse layout");
static_assert(sizeof(RcxDrvReadCr3Response) == 16, "ReadCr3Response layout");
static_assert(sizeof(RcxDrvVtopRequest) == 16, "VtopRequest layout");
static_assert(sizeof(RcxDrvVtopResponse) == 48, "VtopResponse layout");
#endif

View File

@@ -283,9 +283,10 @@ function Find-MinGWDirectory {
$toolsDir = Join-Path $qtRoot "Tools"
if (Test-Path $toolsDir) {
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
$_.Name -match 'mingw'
}
$_.Name -match '^mingw\d+_\d+$'
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
foreach ($dir in $mingwToolDirs) {
$testBin = Join-Path $dir.FullName "bin\g++.exe"

View File

@@ -318,10 +318,10 @@ $qtRoot = Split-Path (Split-Path $selectedQtDir -Parent) -Parent
$toolsDir = Join-Path $qtRoot "Tools"
if (Test-Path $toolsDir) {
# Look for MinGW tools directory
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
$_.Name -match 'mingw'
}
$_.Name -match '^mingw\d+_\d+$'
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
foreach ($dir in $mingwToolDirs) {
$testBin = Join-Path $dir.FullName "bin\g++.exe"

View File

@@ -273,6 +273,7 @@ private:
// Identifier or hex literal disambiguation.
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
// Otherwise → backtrack and parse as hex number.
// If the identifier is followed by '(', try to parse as a built-in function call.
bool parseIdentifierOrHex(uint64_t& result) {
int start = m_pos;
bool hasNonHex = false;
@@ -292,6 +293,11 @@ private:
return parseHexNumber(result);
}
// Check for function call syntax: identifier '(' args ')'
skipSpaces();
if (peek() == '(')
return parseFunctionCall(token, result);
// It's an identifier — resolve via callback
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
result = 0;
@@ -305,6 +311,71 @@ private:
return true;
}
// Built-in function call: vtop(pid, va), cr3(pid), phys(addr)
bool parseFunctionCall(const QString& name, uint64_t& result) {
advance(); // skip '('
if (name == QStringLiteral("vtop")) {
// vtop(pid, virtualAddress) → physical address
uint64_t pid = 0;
if (!parseBitwiseOr(pid)) return false;
skipSpaces();
if (peek() != ',')
return fail("vtop() requires 2 arguments: vtop(pid, va)");
advance(); // skip ','
uint64_t va = 0;
if (!parseBitwiseOr(va)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->vtop) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->vtop((uint32_t)pid, va, &ok);
if (!ok)
return fail(QStringLiteral("vtop(0x%1, 0x%2) failed")
.arg(pid, 0, 16).arg(va, 0, 16));
return true;
}
if (name == QStringLiteral("cr3")) {
// cr3(pid) → CR3 value
uint64_t pid = 0;
if (!parseBitwiseOr(pid)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->cr3) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->cr3((uint32_t)pid, &ok);
if (!ok)
return fail(QStringLiteral("cr3(%1) failed").arg(pid));
return true;
}
if (name == QStringLiteral("phys")) {
// phys(addr) → read 8 bytes from physical address
uint64_t addr = 0;
if (!parseBitwiseOr(addr)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->physRead) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->physRead(addr, &ok);
if (!ok)
return fail(QStringLiteral("phys(0x%1) failed").arg(addr, 0, 16));
return true;
}
return fail(QStringLiteral("unknown function '%1'").arg(name));
}
// '[' bitwiseOr ']' — read the pointer value at the computed address
bool parseDereference(uint64_t& result) {
advance(); // skip '['

View File

@@ -16,6 +16,11 @@ struct AddressParserCallbacks {
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
// Kernel paging functions (optional — only wired when kernel provider active)
std::function<uint64_t(uint32_t pid, uint64_t va, bool* ok)> vtop;
std::function<uint64_t(uint32_t pid, bool* ok)> cr3;
std::function<uint64_t(uint64_t physAddr, bool* ok)> physRead;
};
class AddressParser {

View File

@@ -695,6 +695,11 @@ void composeParent(ComposeState& state, const NodeTree& tree,
*ok = false;
return 0;
};
cbs.resolveModule = [&prov](const QString& name, bool* ok) -> uint64_t {
uint64_t base = prov.symbolToAddress(name);
*ok = (base != 0);
return base;
};
return cbs;
};
@@ -827,6 +832,43 @@ void composeParent(ComposeState& state, const NodeTree& tree,
}
}
// Static pointer: read pointer value at evaluated addr, expand ref struct
if (exprOk && sf.refId != 0
&& (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)) {
int psz = sf.byteSize();
uint64_t ptrVal = 0;
if (prov.isValid() && psz > 0 && prov.isReadable(staticAddr, psz)) {
ptrVal = (sf.kind == NodeKind::Pointer32)
? (uint64_t)prov.readU32(staticAddr) : prov.readU64(staticAddr);
if (ptrVal == UINT64_MAX || (sf.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
ptrVal = 0;
}
// Relative pointer (RVA): target = base + value
if (sf.isRelative && ptrVal != 0)
ptrVal += absAddr;
if (ptrVal != 0) {
uint64_t pBase = ptrVal;
bool ptrReadable = prov.isReadable(pBase, 1);
static NullProvider s_nullProv2;
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv2);
if (!ptrReadable) pBase = 0;
int refIdx = tree.indexOfId(sf.refId);
if (refIdx >= 0) {
const Node& ref = tree.nodes[refIdx];
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
uint64_t savedPtrBase = state.currentPtrBase;
state.currentPtrBase = pBase;
composeParent(state, tree, childProv, refIdx,
childDepth, pBase, ref.id,
/*isArrayChild=*/true);
state.currentPtrBase = savedPtrBase;
}
}
}
}
// Footer line: "};"
{
LineMeta flm;
@@ -893,6 +935,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
&& node.refId != 0) {
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
if (node.isRelative)
ptrTypeOverride += QStringLiteral(" rva");
// Check if this pointer has materialized children (from materializeRefChildren)
const QVector<int>& ptrChildren = childIndices(state, node.id);
@@ -961,7 +1005,10 @@ void composeNode(ComposeState& state, const NodeTree& tree,
}
}
// Pointer target address is used directly (absolute)
// Relative pointer (RVA): target = base + value
if (node.isRelative && ptrVal != 0)
ptrVal += base;
uint64_t pBase = ptrVal;
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);

View File

@@ -17,6 +17,7 @@
#include <QFileDialog>
#include <QMessageBox>
#include <QSettings>
#include <QRegularExpression>
#include <QtConcurrent/QtConcurrentRun>
#include <limits>
@@ -441,13 +442,35 @@ void RcxController::connectEditor(RcxEditor* editor) {
*ok = prov->read(addr, &val, ptrSz);
return val;
};
// Wire kernel paging callbacks if provider supports it
if (prov->hasKernelPaging()) {
cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t {
Q_UNUSED(pid);
auto r = prov->translateAddress(va);
*ok = r.valid;
return r.physical;
};
cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t {
Q_UNUSED(pid);
uint64_t cr3 = prov->getCr3();
*ok = (cr3 != 0);
return cr3;
};
cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t {
auto entries = prov->readPageTable(physAddr, 0, 1);
*ok = !entries.isEmpty();
return entries.isEmpty() ? 0 : entries[0];
};
}
}
auto result = AddressParser::evaluate(s, m_doc->tree.pointerSize, &cbs);
if (result.ok && result.value != m_doc->tree.baseAddress) {
uint64_t oldBase = m_doc->tree.baseAddress;
QString oldFormula = m_doc->tree.baseAddressFormula;
// Store formula if input uses module/deref syntax, otherwise clear
QString newFormula = (s.contains('<') || s.contains('[')) ? s : QString();
// Store formula if input uses module/deref/kernel-function syntax
static const QRegularExpression formulaRx(
QStringLiteral("[<\\[]|\\b(?:vtop|cr3|phys)\\s*\\("));
QString newFormula = formulaRx.match(s).hasMatch() ? s : QString();
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula}));
}
@@ -2006,6 +2029,19 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
if (addedQuickConvert)
menu.addSeparator();
// ── Hex byte / ASCII inline editing ──
if (isHexNode(node.kind) && m_doc->provider->isWritable()) {
menu.addAction(icon("edit.svg"), "Edit He&x Bytes", [editor, line]() {
editor->setHexEditPending(true);
editor->beginInlineEdit(EditTarget::Value, line);
});
menu.addAction(icon("edit.svg"), "Edit &ASCII", [editor, line]() {
editor->setHexEditPending(true);
editor->beginInlineEdit(EditTarget::Name, line);
});
menu.addSeparator();
}
// ── Edit Value / Rename / Change Type ──
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
&& !isHexNode(node.kind)
@@ -2016,9 +2052,11 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
});
}
menu.addAction(icon("rename.svg"), "Re&name\tF2", [editor, line]() {
editor->beginInlineEdit(EditTarget::Name, line);
});
if (!isHexNode(node.kind)) {
menu.addAction(icon("rename.svg"), "Re&name\tF2", [editor, line]() {
editor->beginInlineEdit(EditTarget::Name, line);
});
}
menu.addAction("Change &Type\tT", [editor, line]() {
editor->beginInlineEdit(EditTarget::Type, line);
@@ -2425,6 +2463,103 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
});
// ── Kernel paging menu items ──
if (m_doc->provider && m_doc->provider->hasKernelPaging()) {
menu.addSeparator();
auto* kernelMenu = menu.addMenu(icon("symbol-key.svg"), "Kernel");
// Show Physical Address — translate the node's VA to physical
if (hasNode) {
uint64_t nodeAddr = m_doc->tree.baseAddress
+ m_doc->tree.computeOffset(nodeIdx);
kernelMenu->addAction("Show Physical Address", [this, nodeAddr, &menu]() {
auto result = m_doc->provider->translateAddress(nodeAddr);
if (result.valid) {
const char* pageSz = result.pageSize == 2 ? "1 GB"
: result.pageSize == 1 ? "2 MB" : "4 KB";
QString msg = QStringLiteral(
"Virtual: 0x%1\n"
"Physical: 0x%2\n"
"Page Size: %3\n\n"
"PML4E: 0x%4\n"
"PDPTE: 0x%5\n"
"PDE: 0x%6\n"
"PTE: 0x%7")
.arg(nodeAddr, 16, 16, QChar('0'))
.arg(result.physical, 16, 16, QChar('0'))
.arg(pageSz)
.arg(result.pml4e, 16, 16, QChar('0'))
.arg(result.pdpte, 16, 16, QChar('0'))
.arg(result.pde, 16, 16, QChar('0'))
.arg(result.pte, 16, 16, QChar('0'));
QMessageBox::information(
qobject_cast<QWidget*>(parent()),
QStringLiteral("Physical Address"), msg);
} else {
QMessageBox::warning(
qobject_cast<QWidget*>(parent()),
QStringLiteral("Translation Failed"),
QStringLiteral("Address 0x%1 is not mapped")
.arg(nodeAddr, 16, 16, QChar('0')));
}
});
}
// Browse Page Tables — open PML4 in a new physical tab
kernelMenu->addAction("Browse Page Tables", [this]() {
uint64_t cr3 = m_doc->provider->getCr3();
if (cr3 == 0) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()),
QStringLiteral("Error"),
QStringLiteral("Failed to read CR3"));
return;
}
emit requestOpenProviderTab(
QStringLiteral("kernelmemory"),
QStringLiteral("phys:%1").arg(cr3, 0, 16),
QStringLiteral("PML4 @ 0x%1").arg(cr3, 0, 16));
});
// Follow Physical Frame — on a PTE bitfield, extract PhysAddr and open
if (hasNode) {
const auto& node = m_doc->tree.nodes[nodeIdx];
if (node.classKeyword == QStringLiteral("bitfield")) {
for (const auto& bf : node.bitfieldMembers) {
if (bf.name == QStringLiteral("PhysAddr")) {
int bitOff = bf.bitOffset;
int bitWid = bf.bitWidth;
uint64_t nodeAddr = m_doc->tree.baseAddress
+ m_doc->tree.computeOffset(nodeIdx);
kernelMenu->addAction("Follow Physical Frame",
[this, nodeAddr, bitOff, bitWid]() {
uint64_t pteValue = 0;
if (!m_doc->provider->read(nodeAddr, &pteValue, 8)) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()),
QStringLiteral("Error"),
QStringLiteral("Failed to read PTE at 0x%1")
.arg(nodeAddr, 0, 16));
return;
}
uint64_t mask = (1ULL << bitWid) - 1;
uint64_t frame = ((pteValue >> bitOff) & mask) << bitOff;
if (frame == 0) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()),
QStringLiteral("Error"),
QStringLiteral("Physical frame is zero (not present?)"));
return;
}
emit requestOpenProviderTab(
QStringLiteral("kernelmemory"),
QStringLiteral("phys:%1").arg(frame, 0, 16),
QStringLiteral("PT @ 0x%1").arg(frame, 0, 16));
});
break;
}
}
}
}
}
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
}
@@ -3193,6 +3328,26 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
*ok = prov->read(addr, &val, ptrSz);
return val;
};
// Wire kernel paging callbacks if provider supports it
if (prov->hasKernelPaging()) {
cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t {
Q_UNUSED(pid); // current provider already targets a specific process
auto r = prov->translateAddress(va);
*ok = r.valid;
return r.physical;
};
cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t {
Q_UNUSED(pid);
uint64_t cr3 = prov->getCr3();
*ok = (cr3 != 0);
return cr3;
};
cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t {
auto entries = prov->readPageTable(physAddr, 0, 1);
*ok = !entries.isEmpty();
return entries.isEmpty() ? 0 : entries[0];
};
}
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, ptrSz, &cbs);
if (result.ok)
m_doc->tree.baseAddress = result.value;
@@ -3315,6 +3470,26 @@ void RcxController::selectSource(const QString& text) {
*ok = prov->read(addr, &val, ptrSz);
return val;
};
// Wire kernel paging callbacks if provider supports it
if (prov->hasKernelPaging()) {
cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t {
Q_UNUSED(pid);
auto r = prov->translateAddress(va);
*ok = r.valid;
return r.physical;
};
cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t {
Q_UNUSED(pid);
uint64_t cr3 = prov->getCr3();
*ok = (cr3 != 0);
return cr3;
};
cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t {
auto entries = prov->readPageTable(physAddr, 0, 1);
*ok = !entries.isEmpty();
return entries.isEmpty() ? 0 : entries[0];
};
}
auto result = AddressParser::evaluate(
m_doc->tree.baseAddressFormula, ptrSz, &cbs);
if (result.ok)

View File

@@ -163,6 +163,8 @@ signals:
void nodeSelected(int nodeIdx);
void selectionChanged(int count);
void contextMenuAboutToShow(QMenu* menu, int line);
void requestOpenProviderTab(const QString& pluginId, const QString& target,
const QString& title);
private:
RcxDocument* m_doc;

View File

@@ -197,6 +197,7 @@ struct Node {
int offset = 0;
bool isStatic = false; // static field — excluded from struct layout
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
bool isRelative = false; // Pointer: target = base + value (RVA) instead of absolute
int arrayLen = 1; // Array: element count
int strLen = 64;
bool collapsed = true;
@@ -242,6 +243,8 @@ struct Node {
o["isStatic"] = true;
if (!offsetExpr.isEmpty())
o["offsetExpr"] = offsetExpr;
if (isRelative)
o["isRelative"] = true;
o["arrayLen"] = arrayLen;
o["strLen"] = strLen;
o["collapsed"] = collapsed;
@@ -283,6 +286,7 @@ struct Node {
n.offset = o["offset"].toInt(0);
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
n.offsetExpr = o["offsetExpr"].toString();
n.isRelative = o["isRelative"].toBool(false);
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
n.collapsed = o["collapsed"].toBool(true);
@@ -677,6 +681,7 @@ namespace cmd {
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
struct ToggleRelative { uint64_t nodeId; bool oldVal, newVal; };
}
using Command = std::variant<
@@ -684,7 +689,7 @@ using Command = std::variant<
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
cmd::ChangeOffsetExpr, cmd::ToggleStatic
cmd::ChangeOffsetExpr, cmd::ToggleStatic, cmd::ToggleRelative
>;
// ── Column spans (for inline editing) ──

View File

@@ -515,7 +515,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
connect(m_sci, &QsciScintilla::textChanged, this, [this]() {
if (!m_editState.active) return;
if (m_updatingComment) return; // Skip queuing during comment update
if (m_editState.target == EditTarget::Value)
if (m_editState.target == EditTarget::Value && !m_editState.hexOverwrite)
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
// Autocomplete for static field expressions — show field names as user types
@@ -1605,7 +1605,8 @@ RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
// Dismiss any open user list / autocomplete popup
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
// Clear edit comment and error marker before deactivating
if (m_editState.target == EditTarget::Value) {
if (m_editState.target == EditTarget::Value
|| (m_editState.hexOverwrite && m_editState.target == EditTarget::Name)) {
setEditComment({}); // Clear to spaces
m_sci->markerDelete(m_editState.line, M_ERR);
}
@@ -2341,6 +2342,10 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
// ── Edit mode key handling ──
bool RcxEditor::handleEditKey(QKeyEvent* ke) {
// Hex/ASCII overwrite mode: fully custom key handling
if (m_editState.hexOverwrite)
return handleHexEditKey(ke);
// User list is handled via userListActivated signal, not here
// SCI_AUTOCACTIVE is for autocomplete, not user lists
@@ -2372,12 +2377,6 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
int line, col;
m_sci->getCursorPosition(&line, &col);
int minCol = m_editState.spanStart;
// Don't allow backing into "0x" prefix
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
QString lineText = getLineText(m_sci, m_editState.line);
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
minCol = m_editState.spanStart + 2;
}
// If there's an active selection, collapse it to the left end (Left only, not Backspace)
if (ke->key() == Qt::Key_Left) {
int sL, sC, eL, eC;
@@ -2405,17 +2404,9 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
if (col >= editEndCol()) return true; // block past end
return false;
}
case Qt::Key_Home: {
int home = m_editState.spanStart;
// Skip "0x" prefix for hex values
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
QString lineText = getLineText(m_sci, m_editState.line);
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
home = m_editState.spanStart + 2;
}
m_sci->setCursorPosition(m_editState.line, home);
case Qt::Key_Home:
m_sci->setCursorPosition(m_editState.line, m_editState.spanStart);
return true;
}
case Qt::Key_End:
m_sci->setCursorPosition(m_editState.line, editEndCol());
return true;
@@ -2440,6 +2431,219 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
}
}
// ── Hex/ASCII overwrite-mode key handling ──
bool RcxEditor::handleHexEditKey(QKeyEvent* ke) {
const bool isHexMode = (m_editState.target == EditTarget::Value);
// isHexMode = true: editing "00 00 00 00 00 00 00 00" (hex bytes)
// isHexMode = false: editing "........" (ASCII preview)
int line, col;
m_sci->getCursorPosition(&line, &col);
const int spanStart = m_editState.spanStart;
const int spanEnd = spanStart + m_editState.original.size();
// Helper: replace a single character and re-apply hex dimming indicator
// (SCI_REPLACETARGET can clear indicators at the replacement position)
auto replaceCharAt = [this](long pos, char ch) {
QByteArray buf(1, ch);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, pos);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, pos + 1);
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
(uintptr_t)1, buf.constData());
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, 1);
};
switch (ke->key()) {
case Qt::Key_Return:
case Qt::Key_Enter:
commitInlineEdit();
return true;
case Qt::Key_Escape:
cancelInlineEdit();
return true;
case Qt::Key_Tab:
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
return true; // block
case Qt::Key_Home:
m_sci->setCursorPosition(line, spanStart);
return true;
case Qt::Key_End: {
// Last data position (last char of span)
int endCol = spanEnd - 1;
if (endCol < spanStart) endCol = spanStart;
m_sci->setCursorPosition(line, endCol);
return true;
}
case Qt::Key_Left: {
if (col <= spanStart) return true;
int newCol = col - 1;
// In hex mode, skip over space separators
if (isHexMode) {
QString lineText = getLineText(m_sci, line);
if (newCol >= spanStart && newCol < lineText.size() && lineText[newCol] == ' ')
newCol--;
}
if (newCol < spanStart) newCol = spanStart;
m_sci->setCursorPosition(line, newCol);
return true;
}
case Qt::Key_Right: {
if (col >= spanEnd - 1) return true;
int newCol = col + 1;
if (isHexMode) {
QString lineText = getLineText(m_sci, line);
if (newCol < spanEnd && newCol < lineText.size() && lineText[newCol] == ' ')
newCol++;
}
if (newCol >= spanEnd) newCol = spanEnd - 1;
m_sci->setCursorPosition(line, newCol);
return true;
}
case Qt::Key_Backspace: {
if (col <= spanStart) return true;
int prevCol = col - 1;
if (isHexMode) {
QString lineText = getLineText(m_sci, line);
if (prevCol >= spanStart && prevCol < lineText.size() && lineText[prevCol] == ' ')
prevCol--;
}
if (prevCol < spanStart) return true;
// Replace previous char with reset value
long pos = posFromCol(m_sci, line, prevCol);
replaceCharAt(pos, isHexMode ? '0' : '.');
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, pos);
return true;
}
case Qt::Key_Delete: {
if (col >= spanEnd) return true;
// Skip space separators in hex mode
if (isHexMode) {
QString lineText = getLineText(m_sci, line);
if (col < lineText.size() && lineText[col] == ' ') return true;
}
// Reset current char
long pos = posFromCol(m_sci, line, col);
replaceCharAt(pos, isHexMode ? '0' : '.');
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, pos);
return true;
}
case Qt::Key_Z:
if (ke->modifiers() & Qt::ControlModifier)
return true; // block Ctrl+Z during hex overwrite
break;
case Qt::Key_V:
if (ke->modifiers() & Qt::ControlModifier) {
QString clip = QApplication::clipboard()->text();
clip.remove('\n');
clip.remove('\r');
if (!clip.isEmpty()) {
QString lineText = getLineText(m_sci, line);
int writeCol = col;
for (int i = 0; i < clip.size() && writeCol < spanEnd; i++) {
QChar ch = clip[i];
if (isHexMode) {
// Skip spaces in paste content
if (ch == ' ') continue;
// Skip over space separators in the target
if (writeCol < lineText.size() && lineText[writeCol] == ' ')
writeCol++;
if (writeCol >= spanEnd) break;
// Only accept hex digits
if (!ch.isDigit() && !(ch >= 'a' && ch <= 'f') && !(ch >= 'A' && ch <= 'F'))
continue;
ch = ch.toUpper();
} else {
// Only accept printable ASCII
if (ch.unicode() < 0x20 || ch.unicode() > 0x7E) continue;
}
long pos = posFromCol(m_sci, line, writeCol);
replaceCharAt(pos, (char)ch.toLatin1());
writeCol++;
// Re-read after each replace for hex space skip
if (isHexMode) lineText = getLineText(m_sci, line);
}
int finalCol = qMin(writeCol, spanEnd - 1);
// In hex mode, if we landed on a space, advance past it
if (isHexMode) {
lineText = getLineText(m_sci, line);
if (finalCol < spanEnd && finalCol < lineText.size() && lineText[finalCol] == ' ')
finalCol++;
if (finalCol >= spanEnd) finalCol = spanEnd - 1;
}
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS,
posFromCol(m_sci, line, finalCol));
}
return true;
}
break;
default:
break;
}
// Character input: overwrite current position and advance
QString text = ke->text();
if (text.isEmpty() || text[0].unicode() < 0x20)
return true; // consume non-printable (block default Scintilla handling)
QChar ch = text[0];
if (isHexMode) {
// Only accept hex digits
if (!ch.isDigit() && !(ch >= 'a' && ch <= 'f') && !(ch >= 'A' && ch <= 'F'))
return true;
ch = ch.toUpper();
// If cursor is on a space, skip to next byte
QString lineText = getLineText(m_sci, line);
int writeCol = col;
if (writeCol < lineText.size() && lineText[writeCol] == ' ')
writeCol++;
if (writeCol >= spanEnd) return true;
// Overwrite current digit
long pos = posFromCol(m_sci, line, writeCol);
replaceCharAt(pos, (char)ch.toLatin1());
// Advance cursor, skip over spaces
int nextCol = writeCol + 1;
lineText = getLineText(m_sci, line);
if (nextCol < spanEnd && nextCol < lineText.size() && lineText[nextCol] == ' ')
nextCol++;
if (nextCol >= spanEnd) nextCol = spanEnd - 1;
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS,
posFromCol(m_sci, line, nextCol));
} else {
// ASCII mode: only printable ASCII
if (ch.unicode() < 0x20 || ch.unicode() > 0x7E)
return true;
if (col >= spanEnd) return true;
// Overwrite current char
long pos = posFromCol(m_sci, line, col);
replaceCharAt(pos, (char)ch.toLatin1());
// Advance cursor
int nextCol = col + 1;
if (nextCol >= spanEnd) nextCol = spanEnd - 1;
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS,
posFromCol(m_sci, line, nextCol));
}
return true;
}
// ── Begin inline edit ──
bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
@@ -2490,14 +2694,39 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
(target == EditTarget::BaseAddress || target == EditTarget::Source
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
return false;
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
// Exception: static field names are always editable (they're function names, not hex labels)
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine)
// Hex nodes: only Type is editable via normal flow (double-click, F2, Enter)
// Exception: context-menu-initiated hex/ASCII edits bypass this via m_hexEditPending
bool isHexEdit = m_hexEditPending && isHexNode(lm->nodeKind) && !lm->isStaticLine
&& (target == EditTarget::Name || target == EditTarget::Value);
m_hexEditPending = false;
if ((target == EditTarget::Name || target == EditTarget::Value)
&& isHexNode(lm->nodeKind) && !lm->isStaticLine && !isHexEdit)
return false;
QString lineText;
NormalizedSpan norm;
if (!resolvedSpanFor(line, target, norm, &lineText)) return false;
if (isHexEdit) {
// Compute hex spans directly (bypasses resolvedSpanFor which also blocks hex)
lineText = getLineText(m_sci, line);
int typeW = lm->effectiveTypeW;
int nameW = lm->effectiveNameW;
int byteCount = sizeForKind(lm->nodeKind);
if (target == EditTarget::Name) {
// ASCII preview: exactly byteCount chars (no trailing-space trim)
ColumnSpan s = nameSpanFor(*lm, typeW, nameW);
if (!s.valid) return false;
norm = {s.start, s.start + byteCount, true};
} else {
// Hex bytes: "XX XX XX..." = byteCount*3-1 chars
ColumnSpan s = valueSpanFor(*lm, lineText.size(), typeW, nameW);
if (!s.valid) return false;
int hexWidth = byteCount * 3 - 1;
norm = {s.start, s.start + hexWidth, true};
}
} else {
if (!resolvedSpanFor(line, target, norm, &lineText)) return false;
}
QString trimmed = lineText.mid(norm.start, norm.end - norm.start);
@@ -2558,6 +2787,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_editState.original = trimmed;
m_editState.linelenAfterReplace = lineText.size();
m_editState.editKind = lm->nodeKind;
m_editState.hexOverwrite = isHexEdit;
if (isVectorKind(lm->nodeKind)) {
m_editState.subLine = vecComponent;
m_editState.editKind = NodeKind::Float;
@@ -2567,14 +2797,14 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_editState.editKind = NodeKind::Float;
}
// Store fixed comment column position for value editing
// Store fixed comment column position for value editing (and hex ASCII edits)
// Use large lineLength so commentCol is always computed (padding added dynamically)
if (target == EditTarget::Value) {
if (target == EditTarget::Value || (isHexEdit && target == EditTarget::Name)) {
ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW);
m_editState.commentCol = cs.valid ? cs.start : -1;
m_editState.lastValidationOk = true; // original value is always valid
} else if (target == EditTarget::BaseAddress) {
m_editState.commentCol = norm.end + 2; // command row has no column layout
m_editState.commentCol = (int)lineText.size() + 2; // after full command row content
} else {
m_editState.commentCol = -1;
}
@@ -2586,12 +2816,14 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
m_sci->setReadOnly(false);
// For value editing: extend line with trailing spaces for the edit comment area
// For value/hex editing: extend line with trailing spaces for the edit comment area
// (comment padding is no longer baked into every line to avoid unnecessary scroll width)
if ((target == EditTarget::Value || target == EditTarget::BaseAddress)
if ((target == EditTarget::Value || target == EditTarget::BaseAddress
|| (isHexEdit && target == EditTarget::Name))
&& m_editState.commentCol >= 0) {
int commentStart = norm.end + 2;
int neededLen = commentStart + kColComment;
int commentStart = m_editState.commentCol;
int commentWidth = (target == EditTarget::BaseAddress) ? 60 : kColComment;
int neededLen = commentStart + commentWidth;
int currentLen = (int)lineText.size();
if (currentLen < neededLen) {
int extend = neededLen - currentLen;
@@ -2619,26 +2851,35 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|| target == EditTarget::PointerTarget
|| target == EditTarget::RootClassType);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
if (!isPicker)
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
ThemeManager::instance().current().selection);
if (!isPicker) {
// Subtle tint derived from theme background (neutral, not blue)
const auto& bg = ThemeManager::instance().current().background;
int shift = (bg.lightness() < 128) ? 25 : -25;
QColor tint(qBound(0, bg.red() + shift, 255),
qBound(0, bg.green() + shift, 255),
qBound(0, bg.blue() + shift, 255));
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, tint);
}
// Use correct UTF-8 position conversion (not lineStart + col!)
m_editState.posStart = posFromCol(m_sci, line, norm.start);
m_editState.posEnd = posFromCol(m_sci, line, norm.end);
// For Value/BaseAddress: skip 0x prefix in selection (select only the number)
long selStart = m_editState.posStart;
if ((target == EditTarget::Value || target == EditTarget::BaseAddress) &&
trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
selStart = m_editState.posStart + 2; // Skip "0x"
}
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, selStart, m_editState.posEnd);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, m_editState.posStart, m_editState.posEnd);
// Hex overwrite: place cursor at start, no selection
if (m_editState.hexOverwrite)
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
// Show initial edit hint in comment column
if (target == EditTarget::Value)
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
else if (target == EditTarget::BaseAddress)
if (target == EditTarget::Value) {
if (m_editState.hexOverwrite)
setEditComment(QStringLiteral("Hex edit: Enter=Save Esc=Cancel"));
else
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
} else if (target == EditTarget::Name && m_editState.hexOverwrite) {
setEditComment(QStringLiteral("ASCII edit: Enter=Save Esc=Cancel"));
} else if (target == EditTarget::BaseAddress)
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
@@ -2664,6 +2905,19 @@ void RcxEditor::clampEditSelection() {
if (m_clampingSelection) return;
m_clampingSelection = true;
// Hex overwrite: collapse any selection to cursor (no selection allowed)
if (m_editState.hexOverwrite) {
int sL, sC, eL, eC;
m_sci->getSelection(&sL, &sC, &eL, &eC);
if (sL != eL || sC != eC) {
int curLine, curCol;
m_sci->getCursorPosition(&curLine, &curCol);
m_sci->setCursorPosition(m_editState.line, curCol);
}
m_clampingSelection = false;
return;
}
int selStartLine, selStartCol, selEndLine, selEndCol;
m_sci->getSelection(&selStartLine, &selStartCol, &selEndLine, &selEndCol);
@@ -2709,8 +2963,11 @@ void RcxEditor::commitInlineEdit() {
int editedLen = m_editState.original.size() + delta;
QString editedText;
if (editedLen > 0)
editedText = lineText.mid(m_editState.spanStart, editedLen).trimmed();
if (editedLen > 0) {
editedText = lineText.mid(m_editState.spanStart, editedLen);
if (!m_editState.hexOverwrite)
editedText = editedText.trimmed();
}
// For Type edits: if nothing changed, commit original
if (m_editState.target == EditTarget::Type && editedText.isEmpty())
@@ -2791,26 +3048,8 @@ void RcxEditor::showSourcePicker() {
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
menuFont.setPointSize(menuFont.pointSize() + zoom);
menu.setFont(menuFont);
menu.addAction("File");
// Add all registered providers from global registry
const auto& providers = ProviderRegistry::instance().providers();
for (const auto& provider : providers)
menu.addAction(provider.name);
// Saved sources below separator (with checkmarks)
if (!m_savedSourceDisplay.isEmpty()) {
menu.addSeparator();
for (int i = 0; i < m_savedSourceDisplay.size(); i++) {
auto* act = menu.addAction(m_savedSourceDisplay[i].text);
act->setCheckable(true);
act->setChecked(m_savedSourceDisplay[i].active);
act->setData(i);
}
menu.addSeparator();
auto* clearAct = menu.addAction("Clear All");
clearAct->setData(QStringLiteral("#clear"));
}
ProviderRegistry::populateSourceMenu(&menu, m_savedSourceDisplay);
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
@@ -2824,11 +3063,13 @@ void RcxEditor::showSourcePicker() {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit();
QString text = sel->text();
if (sel->data().toString() == QStringLiteral("#clear"))
text = QStringLiteral("#clear");
else if (sel->data().isValid())
text = QStringLiteral("#saved:") + QString::number(sel->data().toInt());
// Route via action data (set by populateSourceMenu)
QString text = sel->data().toString();
if (text.isEmpty()) {
// Plugin action (e.g. "Unload Driver") — already handled by its own lambda
cancelInlineEdit();
return;
}
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
} else {
cancelInlineEdit();
@@ -3540,7 +3781,7 @@ void RcxEditor::setEditComment(const QString& comment) {
// Place comment 2 spaces after current value, prefixed with //
int valueEnd = editEndCol();
int startCol = valueEnd + 2; // 2 spaces after value
int startCol = qMax(valueEnd + 2, m_editState.commentCol);
int endCol = lineText.size();
int availWidth = endCol - startCol;
if (availWidth <= 0) { m_updatingComment = false; return; }
@@ -3589,7 +3830,12 @@ void RcxEditor::validateEditLive() {
if (isValid) {
m_sci->markerDelete(m_editState.line, M_ERR);
if (isSelected) m_sci->markerAdd(m_editState.line, M_SELECTED);
if (stateChanged) setEditComment("Enter=Save Esc=Cancel");
if (stateChanged) {
if (m_editState.target == EditTarget::BaseAddress)
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
else
setEditComment("Enter=Save Esc=Cancel");
}
} else {
if (isSelected) m_sci->markerDelete(m_editState.line, M_SELECTED);
m_sci->markerAdd(m_editState.line, M_ERR);

View File

@@ -1,5 +1,6 @@
#pragma once
#include "core.h"
#include "providerregistry.h"
#include "themes/theme.h"
#include <QWidget>
#include <QSet>
@@ -12,11 +13,6 @@ class QsciLexerCPP;
namespace rcx {
struct SavedSourceDisplay {
QString text;
bool active = false;
};
class RcxEditor : public QWidget {
Q_OBJECT
public:
@@ -51,6 +47,7 @@ public:
bool isEditing() const { return m_editState.active; }
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
void cancelInlineEdit();
void setHexEditPending(bool v) { m_hexEditPending = v; }
void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; }
void applySelectionOverlay(const QSet<uint64_t>& selIds);
@@ -143,6 +140,7 @@ private:
NodeKind editKind = NodeKind::Int32;
int commentCol = -1; // fixed comment column (stored at edit start)
bool lastValidationOk = true; // track state to avoid redundant updates
bool hexOverwrite = false; // true for hex-byte / ASCII-preview fixed-length editing
};
InlineEditState m_editState;
QStringList m_staticCompletions; // autocomplete words for StaticExpr editing
@@ -171,6 +169,9 @@ private:
long m_findPos = 0;
void hideFindBar();
// ── Hex inline edit ──
bool m_hexEditPending = false; // set by context menu before calling beginInlineEdit
// ── Reentrancy guards ──
bool m_applyingDocument = false;
bool m_clampingSelection = false;
@@ -195,6 +196,7 @@ private:
int editEndCol() const;
bool handleNormalKey(QKeyEvent* ke);
bool handleEditKey(QKeyEvent* ke);
bool handleHexEditKey(QKeyEvent* ke);
void showTypeAutocomplete();
void showSourcePicker();
void showTypeListFiltered(const QString& filter);

File diff suppressed because it is too large Load Diff

View File

@@ -6,17 +6,83 @@
namespace rcx {
// Generate C++ struct definitions for a single root struct and all
// nested/referenced types reachable from it.
// ── Code output format ──
enum class CodeFormat : int {
CppHeader = 0, // C/C++ struct definitions
RustStruct, // Rust #[repr(C)] struct definitions
DefineOffsets, // #define ClassName_FieldName 0xNN
CSharpStruct, // C# [StructLayout] with [FieldOffset]
PythonCtypes, // Python ctypes.Structure
_Count
};
enum class CodeScope : int {
Current = 0, // Just the selected struct
WithChildren, // Selected struct + all referenced types
FullSdk, // All root-level structs
_Count
};
const char* codeFormatName(CodeFormat fmt);
const char* codeFormatFileFilter(CodeFormat fmt);
const char* codeScopeName(CodeScope scope);
// ── Format-aware dispatch (calls the appropriate backend) ──
QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// Render rootStructId + all struct types reachable from it
QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCodeAll(CodeFormat fmt, const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// ── Individual backends ──
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// Generate C++ struct definitions for every root-level struct (full SDK).
QString renderCppTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRust(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRustTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRustAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderDefines(const NodeTree& tree, uint64_t rootStructId);
QString renderDefinesTree(const NodeTree& tree, uint64_t rootStructId);
QString renderDefinesAll(const NodeTree& tree);
QString renderCSharp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCSharpAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderPython(const NodeTree& tree, uint64_t rootStructId);
QString renderPythonTree(const NodeTree& tree, uint64_t rootStructId);
QString renderPythonAll(const NodeTree& tree);
// Null generator placeholder (returns empty string).
QString renderNull(const NodeTree& tree, uint64_t rootStructId);

View File

@@ -10,8 +10,9 @@
#define RCX_PLUGIN_EXPORT __attribute__((visibility("default")))
#endif
// Forward declaration
// Forward declarations
namespace rcx { class Provider; }
class QMenu;
/**
* Plugin interface for Reclass
@@ -129,6 +130,13 @@ public:
* @return true if enumerateProcesses() should be called
*/
virtual bool providesProcessList() const { return false; }
/**
* Add plugin-specific actions to the source menu (optional).
* Called each time the source menu is shown. Only add items when relevant
* (e.g., "Unload Driver" only when the driver is loaded).
*/
virtual void populatePluginMenu(QMenu*) {}
};
// Plugin factory function signature

View File

@@ -58,6 +58,7 @@
#include <windowsx.h>
#include <dwmapi.h>
#include <dbghelp.h>
#include <shellapi.h>
#include <cstdio>
static void setDarkTitleBar(QWidget* widget) {
@@ -552,7 +553,7 @@ void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setWindowTitle("Reclass");
resize(1200, 800);
resize(1080, 720);
#ifndef __APPLE__
// Frameless window with system menu (Alt+Space) and min/max/close support.
@@ -605,24 +606,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createWorkspaceDock();
createScannerDock();
// Hidden sentinel dock — never visible, only used to force Qt to create a
// QTabBar when the first document dock is added (Qt only creates tab bars
// via tabifyDockWidget). Immediately hidden after tabification so it takes
// zero layout space. An event filter on the QTabBar keeps it visible.
{
m_sentinelDock = new QDockWidget(this);
m_sentinelDock->setObjectName(QStringLiteral("_sentinel"));
m_sentinelDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
auto* sw = new QWidget(m_sentinelDock);
sw->setFixedSize(0, 0);
m_sentinelDock->setWidget(sw);
auto* stb = new QWidget(m_sentinelDock);
stb->setFixedHeight(0);
m_sentinelDock->setTitleBarWidget(stb);
addDockWidget(Qt::TopDockWidgetArea, m_sentinelDock);
m_sentinelDock->hide(); // hidden = zero layout space
}
createMenus();
createStatusBar();
@@ -749,6 +732,10 @@ void MainWindow::createMenus() {
Qt5Qt6AddAction(importMenu, "&PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb);
auto* exportMenu = file->addMenu("E&xport");
Qt5Qt6AddAction(exportMenu, "&C++ Header...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCpp);
Qt5Qt6AddAction(exportMenu, "&Rust Structs...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportRust);
Qt5Qt6AddAction(exportMenu, "#&define Offsets...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportDefines);
Qt5Qt6AddAction(exportMenu, "C&# Structs...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCSharp);
Qt5Qt6AddAction(exportMenu, "&Python ctypes...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportPython);
Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
// Examples submenu — scan once at init
{
@@ -769,6 +756,52 @@ void MainWindow::createMenus() {
file->addSeparator();
Qt5Qt6AddAction(file, "&Close Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
file->addSeparator();
#ifdef _WIN32
{
// "Relaunch as Administrator" — hidden when already elevated
bool elevated = false;
HANDLE token = nullptr;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
TOKEN_ELEVATION elev{};
DWORD sz = sizeof(elev);
if (GetTokenInformation(token, TokenElevation, &elev, sizeof(elev), &sz))
elevated = (elev.TokenIsElevated != 0);
CloseHandle(token);
}
if (!elevated) {
Qt5Qt6AddAction(file, "Relaunch as &Administrator",
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A),
makeIcon(":/vsicons/shield.svg"), this, [this]() {
wchar_t exePath[MAX_PATH];
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
SHELLEXECUTEINFOW sei{};
sei.cbSize = sizeof(sei);
sei.lpVerb = L"runas";
sei.lpFile = exePath;
sei.nShow = SW_SHOWNORMAL;
if (ShellExecuteExW(&sei))
QCoreApplication::quit();
// If UAC was cancelled, do nothing
});
file->addSeparator();
}
}
#endif
m_sourceMenu = file->addMenu("&Data Source");
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
connect(m_sourceMenu, &QMenu::triggered, this, [this](QAction* act) {
auto* c = activeController();
if (!c) return;
QString data = act->data().toString();
if (data.isEmpty()) return; // plugin actions handle themselves via lambda
if (data == QStringLiteral("#clear"))
c->clearSources();
else if (data.startsWith(QStringLiteral("#saved:")))
c->switchSource(data.mid(7).toInt());
else
c->selectSource(data);
});
file->addSeparator();
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
// Edit
@@ -779,20 +812,26 @@ void MainWindow::createMenus() {
// View
auto* view = m_menuBar->addMenu("&View");
Qt5Qt6AddAction(view, "&Reset Windows", QKeySequence::UnknownKey, QIcon(), this, [this](bool) {
// Re-tabify all doc docks into a single group
if (m_docDocks.size() < 2) return;
// Re-tabify all doc docks into a single group (collapses splits)
if (m_docDocks.isEmpty()) return;
auto* first = m_docDocks.first();
for (int i = 1; i < m_docDocks.size(); ++i) {
tabifyDockWidget(first, m_docDocks[i]);
m_docDocks[i]->show();
}
// Merge all sentinels back; keep only the first, delete extras
for (int i = 0; i < m_sentinelDocks.size(); ++i) {
if (i == 0)
tabifyDockWidget(first, m_sentinelDocks[i]);
else
delete m_sentinelDocks[i];
}
if (m_sentinelDocks.size() > 1)
m_sentinelDocks.resize(1);
if (m_activeDocDock) m_activeDocDock->raise();
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
});
view->addSeparator();
m_sourceMenu = view->addMenu("&Data Source");
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
view->addSeparator();
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
auto* fontGroup = new QActionGroup(this);
fontGroup->setExclusive(true);
@@ -881,7 +920,8 @@ void MainWindow::createMenus() {
const auto mcpName = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
m_mcpAction = Qt5Qt6AddAction(tools, mcpName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
tools->addSeparator();
Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog);
Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this,
static_cast<void(MainWindow::*)()>(&MainWindow::showOptionsDialog));
// Plugins
auto* plugins = m_menuBar->addMenu("&Plugins");
@@ -1203,7 +1243,7 @@ void MainWindow::createStatusBar() {
m_statusLabel->setContentsMargins(0, 0, 0, 0);
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
// View toggle is now per-pane via QTabWidget tab bar (Reclass / C/C++ tabs)
// View toggle is now per-pane via QTabWidget tab bar (Reclass / Code tabs)
sb->tabRow = nullptr;
sb->label = m_statusLabel;
@@ -1423,7 +1463,87 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
sci->setFocus();
});
pane.tabWidget->addTab(pane.renderedContainer, "C/C++"); // index 1
pane.tabWidget->addTab(pane.renderedContainer, "Code"); // index 1
// Corner widget: format combo + gear icon
{
const auto& ct = ThemeManager::instance().current();
QSettings cs("Reclass", "Reclass");
QString ef = cs.value("font", "JetBrains Mono").toString();
auto* cornerWidget = new QWidget;
auto* cornerLayout = new QHBoxLayout(cornerWidget);
cornerLayout->setContentsMargins(0, 0, 4, 0);
cornerLayout->setSpacing(2);
pane.fmtCombo = new QComboBox;
for (int fi = 0; fi < static_cast<int>(CodeFormat::_Count); ++fi)
pane.fmtCombo->addItem(codeFormatName(static_cast<CodeFormat>(fi)));
pane.fmtCombo->setCurrentIndex(cs.value("codeFormat", 0).toInt());
pane.fmtCombo->setFixedHeight(22);
pane.fmtCombo->setStyleSheet(QStringLiteral(
"QComboBox { background: %1; color: %2; border: 1px solid %3;"
" padding: 1px 6px; font-family: '%6'; font-size: 9pt; }"
"QComboBox::drop-down { border: none; width: 14px; }"
"QComboBox::down-arrow { image: url(:/vsicons/chevron-down.svg);"
" width: 10px; height: 10px; }"
"QComboBox QAbstractItemView { background: %4; color: %2;"
" selection-background-color: %5; border: 1px solid %3; }")
.arg(ct.background.name(), ct.textMuted.name(), ct.border.name(),
ct.backgroundAlt.name(), ct.hover.name(), ef));
pane.fmtGear = new QToolButton;
pane.fmtGear->setIcon(QIcon(":/vsicons/settings-gear.svg"));
pane.fmtGear->setFixedSize(22, 22);
pane.fmtGear->setToolTip("Generator Options");
pane.fmtGear->setStyleSheet(QStringLiteral(
"QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
"QToolButton:hover { background: %4; }")
.arg(ct.background.name(), ct.textMuted.name(), ct.border.name(),
ct.hover.name()));
pane.scopeCombo = new QComboBox;
for (int si = 0; si < static_cast<int>(CodeScope::_Count); ++si)
pane.scopeCombo->addItem(codeScopeName(static_cast<CodeScope>(si)));
pane.scopeCombo->setCurrentIndex(cs.value("codeScope", 0).toInt());
pane.scopeCombo->setFixedHeight(22);
pane.scopeCombo->setStyleSheet(pane.fmtCombo->styleSheet());
cornerLayout->addWidget(pane.fmtCombo);
cornerLayout->addWidget(pane.scopeCombo);
cornerLayout->addWidget(pane.fmtGear);
pane.tabWidget->setCornerWidget(cornerWidget, Qt::BottomRightCorner);
cornerWidget->setVisible(false); // hidden until Code tab selected
auto refreshAllRendered = [this]() {
for (auto& tab : m_tabs)
for (auto& p : tab.panes)
if (p.viewMode == VM_Rendered)
updateRenderedView(tab, p);
};
connect(pane.fmtCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, refreshAllRendered](int idx) {
QSettings("Reclass", "Reclass").setValue("codeFormat", idx);
refreshAllRendered();
for (auto& tab : m_tabs)
for (auto& p : tab.panes)
if (p.fmtCombo && p.fmtCombo->currentIndex() != idx)
p.fmtCombo->setCurrentIndex(idx);
});
connect(pane.scopeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, refreshAllRendered](int idx) {
QSettings("Reclass", "Reclass").setValue("codeScope", idx);
refreshAllRendered();
for (auto& tab : m_tabs)
for (auto& p : tab.panes)
if (p.scopeCombo && p.scopeCombo->currentIndex() != idx)
p.scopeCombo->setCurrentIndex(idx);
});
connect(pane.fmtGear, &QToolButton::clicked, this, [this]() {
showOptionsDialog(2); // Generator page
});
}
pane.tabWidget->setCurrentIndex(0);
pane.viewMode = VM_Reclass;
@@ -1437,6 +1557,10 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
SplitPane* p = findPaneByTabWidget(tw);
if (!p) return;
// Show/hide corner controls (format combo, scope combo, gear)
if (auto* cw = tw->cornerWidget(Qt::BottomRightCorner))
cw->setVisible(index == 1);
p->viewMode = (index == 1) ? VM_Rendered : VM_Reclass;
// Sync status bar buttons if this is the active pane
@@ -1538,6 +1662,21 @@ QString MainWindow::tabTitle(const TabState& tab) const {
return name;
}
// Create a sentinel dock — invisible tab that keeps Qt's tab bar on-screen
// when only 1 real dock remains in a group.
QDockWidget* MainWindow::createSentinelDock() {
auto* sentinel = new QDockWidget(this);
sentinel->setObjectName(QStringLiteral("_sentinel_%1").arg(quintptr(sentinel), 0, 16));
sentinel->setFeatures(QDockWidget::NoDockWidgetFeatures);
sentinel->setWidget(new QWidget(sentinel));
auto* stb = new QWidget(sentinel);
stb->setFixedHeight(0);
sentinel->setTitleBarWidget(stb);
sentinel->setWindowTitle(QStringLiteral("\u200B"));
m_sentinelDocks.append(sentinel);
return sentinel;
}
QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto* splitter = new QSplitter(Qt::Horizontal);
splitter->setHandleWidth(1);
@@ -1658,19 +1797,22 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
});
// Tabify with existing doc docks, or add to top area
if (!m_docDocks.isEmpty())
if (!m_docDocks.isEmpty()) {
tabifyDockWidget(m_docDocks.last(), dock);
else
} else {
addDockWidget(Qt::TopDockWidgetArea, dock);
// Bootstrap: tabify the hidden sentinel with the first doc dock so Qt
// creates a QTabBar. Then hide sentinel (zero layout space). The event
// filter in eventFilter() keeps the tab bar visible even at count==1.
if (m_sentinelDock && m_docDocks.isEmpty()) {
m_sentinelDock->show();
tabifyDockWidget(dock, m_sentinelDock);
m_sentinelDock->hide();
dock->raise();
// Deferred sentinel — must wait for Qt to finish laying out the
// first doc dock before tabifyDockWidget can merge them into tabs.
QTimer::singleShot(0, this, [this, dock]() {
if (!dock->isVisible()) return;
// Check if this dock already has a sentinel (e.g. second createTab raced)
for (auto* td : tabifiedDockWidgets(dock))
if (m_sentinelDocks.contains(static_cast<QDockWidget*>(td))) return;
auto* sentinel = createSentinelDock();
tabifyDockWidget(dock, sentinel);
dock->raise();
setupDockTabBars();
});
}
m_docDocks.append(dock);
@@ -1790,6 +1932,33 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
menu->addAction("Close Tab", [dock]() { dock->close(); });
});
// Open a new tab with a plugin-provided provider (e.g. kernel physical memory)
connect(ctrl, &RcxController::requestOpenProviderTab,
this, [this](const QString& pluginId, const QString& target,
const QString& title) {
auto* newDoc = new RcxDocument(this);
QByteArray data(4096, '\0');
newDoc->loadData(data);
newDoc->tree.baseAddress = 0;
auto* newDock = createTab(newDoc);
auto it = m_tabs.find(newDock);
if (it != m_tabs.end()) {
it->ctrl->attachViaPlugin(pluginId, target);
// Try to load PageTables.rcx template for physical kernel tabs
QString examplesPath = QCoreApplication::applicationDirPath()
+ QStringLiteral("/examples/PageTables.rcx");
if (QFile::exists(examplesPath))
newDoc->load(examplesPath);
// Set base address from provider (template has baseAddress=0,
// but we want to start at the target physical address)
if (newDoc->provider)
newDoc->tree.baseAddress = newDoc->provider->base();
}
newDock->setWindowTitle(title);
rebuildWorkspaceModelNow();
});
// Update rendered panes and workspace on document changes and undo/redo
// Use QPointer to guard against dock being destroyed before deferred timer fires
QPointer<QDockWidget> dockGuard = dock;
@@ -1908,11 +2077,19 @@ void MainWindow::setupDockTabBars() {
.arg(theme.background.name(), theme.border.name(), theme.hover.name()));
}
// Force tab bar visible (event filter keeps it alive, belt-and-suspenders)
tabBar->show();
// Hide sentinel tabs so user sees only real doc tabs.
// Qt's updateTabBar() rebuilds tabs each layout pass, resetting
// visibility, so we must re-hide every call.
static const QString sentinelTitle = QStringLiteral("\u200B");
for (int i = 0; i < tabBar->count(); ++i) {
if (tabBar->tabText(i) == sentinelTitle)
tabBar->setTabVisible(i, false);
}
// Install tab buttons for any tab that doesn't have them yet
for (int i = 0; i < tabBar->count(); ++i) {
if (tabBar->tabText(i) == sentinelTitle)
continue;
auto* existing = qobject_cast<DockTabButtons*>(
tabBar->tabButton(i, QTabBar::RightSide));
if (existing) continue;
@@ -1996,8 +2173,11 @@ void MainWindow::setupDockTabBars() {
menu.addSeparator();
// New Document Groups (only if >1 tab)
if (tabBar->count() > 1) {
// New Document Groups (only if >1 visible tab — excludes sentinels)
int visibleTabs = 0;
for (int i = 0; i < tabBar->count(); ++i)
if (tabBar->isTabVisible(i)) ++visibleTabs;
if (visibleTabs > 1) {
menu.addAction(makeIcon(":/vsicons/split-horizontal.svg"),
"New Horizontal Document Group",
[this, target]() {
@@ -2016,7 +2196,12 @@ void MainWindow::setupDockTabBars() {
}
if (docks.size() >= 2)
resizeDocks(docks, sizes, Qt::Horizontal);
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
QTimer::singleShot(0, this, [this, target]() {
auto* s = createSentinelDock();
tabifyDockWidget(target, s);
target->raise();
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
});
});
menu.addAction(makeIcon(":/vsicons/split-vertical.svg"),
"New Vertical Document Group",
@@ -2036,7 +2221,12 @@ void MainWindow::setupDockTabBars() {
}
if (docks.size() >= 2)
resizeDocks(docks, sizes, Qt::Vertical);
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
QTimer::singleShot(0, this, [this, target]() {
auto* s = createSentinelDock();
tabifyDockWidget(target, s);
target->raise();
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
});
});
}
@@ -2046,25 +2236,6 @@ void MainWindow::setupDockTabBars() {
}
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
// Keep dock tab bars visible even when Qt wants to hide them (count==1).
// Qt's QMainWindowLayout calls setVisible(false) on the QTabBar when only
// one dock remains in a tab group. We catch the resulting Hide event and
// immediately re-show the tab bar, provided at least one doc dock is docked.
if (event->type() == QEvent::Hide && !m_tabBarShowGuard) {
if (auto* tabBar = qobject_cast<QTabBar*>(obj)) {
if (tabBar->parent() == this && tabBar->count() >= 1) {
bool hasDockedDoc = false;
for (auto* d : m_docDocks)
if (!d->isFloating() && d->isVisible()) { hasDockedDoc = true; break; }
if (hasDockedDoc) {
m_tabBarShowGuard = true;
tabBar->show();
m_tabBarShowGuard = false;
return true;
}
}
}
}
if (event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::MiddleButton) {
@@ -2542,7 +2713,7 @@ void MainWindow::applyTheme(const Theme& theme) {
}
}
// Restyle per-pane view tab bars (Reclass / C++)
// Restyle per-pane view tab bars (Reclass / Code)
{
QString editorFont = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
QString paneTabStyle = QStringLiteral(
@@ -2558,10 +2729,31 @@ void MainWindow::applyTheme(const Theme& theme) {
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
theme.backgroundAlt.name(), theme.hover.name(), theme.indHoverSpan.name(),
editorFont);
QString comboStyle = QStringLiteral(
"QComboBox { background: %1; color: %2; border: 1px solid %3;"
" padding: 1px 6px; font-family: '%6'; font-size: 9pt; }"
"QComboBox::drop-down { border: none; width: 14px; }"
"QComboBox::down-arrow { image: url(:/vsicons/chevron-down.svg);"
" width: 10px; height: 10px; }"
"QComboBox QAbstractItemView { background: %4; color: %2;"
" selection-background-color: %5; border: 1px solid %3; }")
.arg(theme.background.name(), theme.textMuted.name(), theme.border.name(),
theme.backgroundAlt.name(), theme.hover.name(), editorFont);
QString gearStyle = QStringLiteral(
"QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
"QToolButton:hover { background: %4; }")
.arg(theme.background.name(), theme.textMuted.name(), theme.border.name(),
theme.hover.name());
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
for (auto& pane : it->panes) {
if (pane.tabWidget)
pane.tabWidget->setStyleSheet(paneTabStyle);
if (pane.fmtCombo)
pane.fmtCombo->setStyleSheet(comboStyle);
if (pane.scopeCombo)
pane.scopeCombo->setStyleSheet(comboStyle);
if (pane.fmtGear)
pane.fmtGear->setStyleSheet(gearStyle);
}
}
}
@@ -2706,7 +2898,7 @@ void MainWindow::applyTheme(const Theme& theme) {
}
}
// Rendered C/C++ views: update lexer colors, paper, margins
// Rendered Code views: update lexer colors, paper, margins
for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) {
auto* sci = pane.rendered;
@@ -2752,7 +2944,9 @@ void MainWindow::editTheme() {
}
// TODO: when adding more and more options, this func becomes very clunky. Fix
void MainWindow::showOptionsDialog() {
void MainWindow::showOptionsDialog() { showOptionsDialog(-1); }
void MainWindow::showOptionsDialog(int initialPage) {
auto& tm = ThemeManager::instance();
OptionsResult current;
current.themeIndex = tm.currentIndex();
@@ -2767,7 +2961,9 @@ void MainWindow::showOptionsDialog() {
current.braceWrap = QSettings("Reclass", "Reclass").value("braceWrap", false).toBool();
OptionsDialog dlg(current, this);
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
if (initialPage >= 0)
dlg.selectPage(initialPage);
if (dlg.exec() != QDialog::Accepted) return;
auto r = dlg.result();
@@ -2863,7 +3059,7 @@ void MainWindow::setEditorFont(const QString& fontName) {
tabBar->update();
}
}
// Pane tab bars (Reclass / C++) — re-apply stylesheet with new font
// Pane tab bars (Reclass / Code) — re-apply stylesheet with new font
// (stylesheet overrides setFont, so font must be in the CSS)
applyTheme(ThemeManager::instance().current());
}
@@ -3038,11 +3234,21 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
const QHash<NodeKind, QString>* aliases =
tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases;
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
CodeFormat fmt = static_cast<CodeFormat>(
QSettings("Reclass", "Reclass").value("codeFormat", 0).toInt());
CodeScope scope = static_cast<CodeScope>(
QSettings("Reclass", "Reclass").value("codeScope", 0).toInt());
QString text;
if (rootId != 0)
text = renderCpp(tab.doc->tree, rootId, aliases, asserts);
else
text = renderCppAll(tab.doc->tree, aliases, asserts);
if (scope == CodeScope::FullSdk) {
text = renderCodeAll(fmt, tab.doc->tree, aliases, asserts);
} else if (rootId != 0) {
if (scope == CodeScope::WithChildren)
text = renderCodeTree(fmt, tab.doc->tree, rootId, aliases, asserts);
else
text = renderCode(fmt, tab.doc->tree, rootId, aliases, asserts);
} else {
text = renderCodeAll(fmt, tab.doc->tree, aliases, asserts);
}
// Scroll restoration: save if same root, reset if different
int restoreLine = 0;
@@ -3113,6 +3319,96 @@ void MainWindow::exportCpp() {
setAppStatus("Exported to " + QFileInfo(path).fileName());
}
// ── Export Rust structs ──
void MainWindow::exportRust() {
auto* tab = activeTab();
if (!tab) return;
QString path = QFileDialog::getSaveFileName(this,
"Export Rust Structs", {}, "Rust Source (*.rs);;All Files (*)");
if (path.isEmpty()) return;
const QHash<NodeKind, QString>* aliases =
tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases;
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
QString text = renderRustAll(tab->doc->tree, aliases, asserts);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, "Export Failed",
"Could not write to: " + path);
return;
}
file.write(text.toUtf8());
setAppStatus("Exported to " + QFileInfo(path).fileName());
}
// ── Export #define offsets ──
void MainWindow::exportDefines() {
auto* tab = activeTab();
if (!tab) return;
QString path = QFileDialog::getSaveFileName(this,
"Export #define Offsets", {}, "C Header (*.h);;All Files (*)");
if (path.isEmpty()) return;
QString text = renderDefinesAll(tab->doc->tree);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, "Export Failed",
"Could not write to: " + path);
return;
}
file.write(text.toUtf8());
setAppStatus("Exported to " + QFileInfo(path).fileName());
}
// ── Export C# structs ──
void MainWindow::exportCSharp() {
auto* tab = activeTab();
if (!tab) return;
QString path = QFileDialog::getSaveFileName(this,
"Export C# Structs", {}, "C# Source (*.cs);;All Files (*)");
if (path.isEmpty()) return;
const QHash<NodeKind, QString>* aliases =
tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases;
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
QString text = renderCSharpAll(tab->doc->tree, aliases, asserts);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, "Export Failed",
"Could not write to: " + path);
return;
}
file.write(text.toUtf8());
setAppStatus("Exported to " + QFileInfo(path).fileName());
}
// ── Export Python ctypes ──
void MainWindow::exportPython() {
auto* tab = activeTab();
if (!tab) return;
QString path = QFileDialog::getSaveFileName(this,
"Export Python ctypes", {}, "Python Source (*.py);;All Files (*)");
if (path.isEmpty()) return;
QString text = renderPythonAll(tab->doc->tree);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, "Export Failed",
"Could not write to: " + path);
return;
}
file.write(text.toUtf8());
setAppStatus("Exported to " + QFileInfo(path).fileName());
}
// ── Export ReClass XML ──
void MainWindow::exportReclassXmlAction() {
@@ -4408,63 +4704,19 @@ void MainWindow::populateSourceMenu() {
m_sourceMenu->clear();
auto* ctrl = activeController();
// Icon map for known provider identifiers
static const QHash<QString, QString> s_providerIcons = {
{QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")},
{QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")},
{QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")},
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
};
auto addSourceAction = [this](const QString& text, const QIcon& icon, auto&& slot) {
auto* act = m_sourceMenu->addAction(icon, text);
act->setIconVisibleInMenu(true);
connect(act, &QAction::triggered, this, std::forward<decltype(slot)>(slot));
return act;
};
addSourceAction(QStringLiteral("File"),
makeIcon(QStringLiteral(":/vsicons/file-binary.svg")),
[this]() {
if (auto* c = activeController()) c->selectSource(QStringLiteral("File"));
});
const auto& providers = ProviderRegistry::instance().providers();
for (const auto& prov : providers) {
QString name = prov.name;
auto it = s_providerIcons.constFind(prov.identifier);
QIcon icon = makeIcon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg"));
QString label = prov.dllFileName.isEmpty()
? name
: QStringLiteral("%1 (%2)").arg(name, prov.dllFileName);
addSourceAction(label, icon, [this, name]() {
if (auto* c = activeController()) c->selectSource(name);
});
}
if (ctrl && !ctrl->savedSources().isEmpty()) {
m_sourceMenu->addSeparator();
for (int i = 0; i < ctrl->savedSources().size(); i++) {
const auto& e = ctrl->savedSources()[i];
auto* act = m_sourceMenu->addAction(
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName));
act->setCheckable(true);
act->setChecked(i == ctrl->activeSourceIndex());
connect(act, &QAction::triggered, this, [this, i]() {
if (auto* c = activeController()) c->switchSource(i);
});
// Build saved sources for the shared menu builder
QVector<SavedSourceDisplay> saved;
if (ctrl) {
const auto& ss = ctrl->savedSources();
for (int i = 0; i < ss.size(); i++) {
SavedSourceDisplay d;
d.text = QStringLiteral("%1 '%2'").arg(ss[i].kind, ss[i].displayName);
d.active = (i == ctrl->activeSourceIndex());
saved.append(d);
}
m_sourceMenu->addSeparator();
auto* clearAct = addSourceAction(QStringLiteral("Clear All"),
makeIcon(QStringLiteral(":/vsicons/clear-all.svg")),
[this]() {
if (auto* c = activeController()) c->clearSources();
});
Q_UNUSED(clearAct);
}
ProviderRegistry::populateSourceMenu(m_sourceMenu, saved);
}
void MainWindow::showPluginsDialog() {

View File

@@ -16,8 +16,10 @@
#include <QLineEdit>
#include <QMap>
#include <QButtonGroup>
#include <QComboBox>
#include <QPushButton>
#include <QTimer>
#include <QToolButton>
#include <Qsci/qsciscintilla.h>
namespace rcx {
@@ -58,6 +60,10 @@ private slots:
void toggleMcp();
void setEditorFont(const QString& fontName);
void exportCpp();
void exportRust();
void exportDefines();
void exportCSharp();
void exportPython();
void exportReclassXmlAction();
void importFromSource();
void importReclassXml();
@@ -65,6 +71,7 @@ private slots:
void showTypeAliasesDialog();
void editTheme();
void showOptionsDialog();
void showOptionsDialog(int initialPage);
public:
// Status bar helpers — separate app / MCP channels
@@ -106,6 +113,9 @@ private:
QLineEdit* findBar = nullptr;
QWidget* findContainer = nullptr;
QWidget* renderedContainer = nullptr;
QComboBox* fmtCombo = nullptr;
QComboBox* scopeCombo = nullptr;
QToolButton* fmtGear = nullptr;
ViewMode viewMode = VM_Reclass;
uint64_t lastRenderedRootId = 0;
};
@@ -120,10 +130,9 @@ private:
QMap<QDockWidget*, TabState> m_tabs;
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
QDockWidget* m_sentinelDock = nullptr; // hidden dock to bootstrap tab bar creation
QVector<QDockWidget*> m_sentinelDocks; // permanent sentinels for always-visible tab bars
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
bool m_closingAll = false; // guards spurious project_new during batch close
bool m_tabBarShowGuard = false; // prevents recursion in event filter re-show
struct ClosingGuard {
bool& flag;
ClosingGuard(bool& f) : flag(f) { flag = true; }
@@ -144,6 +153,7 @@ private:
TabState* activeTab();
TabState* tabByIndex(int index);
int tabCount() const { return m_tabs.size(); }
QDockWidget* createSentinelDock();
QDockWidget* createTab(RcxDocument* doc);
QString tabTitle(const TabState& tab) const;
void setupDockTabBars();

View File

@@ -1,17 +1,42 @@
#include "mcp_bridge.h"
#include "addressparser.h"
#include "core.h"
#include "controller.h"
#include "generator.h"
#include "mainwindow.h"
#include "scanner.h"
#include <QCoreApplication>
#include <QSettings>
#include <QTimer>
#include <QDebug>
#include <cstring>
#include <algorithm>
namespace rcx {
static constexpr int kMaxReadBuffer = 10 * 1024 * 1024; // 10 MB
// Parse a number from JSON; accepts string (hex "0x..." or decimal) or number.
// Use for offset, length, pid, limit, tabIndex, etc. to avoid double precision loss
// and to allow clients to send exact values as decimal/hex strings.
static int64_t parseInteger(const QJsonValue& v, int64_t defaultVal = 0) {
if (v.isUndefined() || v.isNull())
return defaultVal;
if (v.isString()) {
QString s = v.toString().trimmed();
if (s.isEmpty())
return defaultVal;
bool ok;
qint64 val = s.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)
? s.mid(2).toLongLong(&ok, 16)
: s.toLongLong(&ok, 10);
return ok ? val : defaultVal;
}
if (v.isDouble())
return static_cast<int64_t>(v.toDouble());
return defaultVal;
}
// ════════════════════════════════════════════════════════════════════
// Construction / lifecycle
// ════════════════════════════════════════════════════════════════════
@@ -23,7 +48,7 @@ McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent)
m_notifyTimer->setSingleShot(true);
m_notifyTimer->setInterval(100);
connect(m_notifyTimer, &QTimer::timeout, this, [this]() {
if (m_client && m_initialized)
if (!m_clients.isEmpty())
sendNotification("notifications/resources/updated",
QJsonObject{{"uri", "project://tree"}});
});
@@ -55,10 +80,15 @@ void McpBridge::start() {
}
void McpBridge::stop() {
if (m_client) {
m_client->disconnectFromServer();
m_client = nullptr;
for (auto& c : m_clients) {
c.socket->disconnect(this);
c.socket->disconnectFromServer();
c.socket->deleteLater();
}
m_clients.clear();
m_currentSender = nullptr;
m_processing = false;
m_pendingRequests.clear();
if (m_server) {
m_server->close();
delete m_server;
@@ -70,55 +100,95 @@ void McpBridge::stop() {
// Connection handling
// ════════════════════════════════════════════════════════════════════
McpBridge::ClientState* McpBridge::findClient(QLocalSocket* sock) {
for (auto& c : m_clients)
if (c.socket == sock) return &c;
return nullptr;
}
void McpBridge::removeClient(QLocalSocket* sock) {
for (int i = 0; i < m_clients.size(); ++i) {
if (m_clients[i].socket == sock) {
sock->disconnect(this);
sock->deleteLater();
m_clients.removeAt(i);
return;
}
}
}
void McpBridge::onNewConnection() {
auto* pending = m_server->nextPendingConnection();
if (!pending) return;
// Single client — disconnect previous
if (m_client) {
m_client->disconnectFromServer();
m_client->deleteLater();
}
m_clients.append({pending, {}, false});
m_client = pending;
m_readBuffer.clear();
m_initialized = false;
connect(m_client, &QLocalSocket::readyRead,
connect(pending, &QLocalSocket::readyRead,
this, &McpBridge::onReadyRead);
connect(m_client, &QLocalSocket::disconnected,
connect(pending, &QLocalSocket::disconnected,
this, &McpBridge::onDisconnected);
qDebug() << "[MCP] Client connected";
qDebug() << "[MCP] Client connected (" << m_clients.size() << "total)";
}
void McpBridge::onReadyRead() {
m_readBuffer.append(m_client->readAll());
auto* sock = qobject_cast<QLocalSocket*>(sender());
auto* cs = findClient(sock);
if (!cs) return;
cs->readBuffer.append(sock->readAll());
if (m_readBuffer.size() > kMaxReadBuffer) {
if (cs->readBuffer.size() > kMaxReadBuffer) {
qWarning() << "[MCP] Read buffer exceeded 10MB, disconnecting client";
m_client->disconnectFromServer();
sock->disconnectFromServer();
return;
}
// Newline-delimited JSON framing (cursor approach avoids quadratic shifting)
int consumed = 0;
while (true) {
int idx = m_readBuffer.indexOf('\n', consumed);
// Extract complete lines from this client's buffer.
// If a request is already in flight (m_processing), queue the line
// instead of processing it -- nested event loops in scanner/tree.apply
// would otherwise let interleaved requests clobber m_currentSender.
while (findClient(sock)) {
cs = findClient(sock);
int idx = cs->readBuffer.indexOf('\n');
if (idx < 0) break;
QByteArray line = m_readBuffer.mid(consumed, idx - consumed).trimmed();
consumed = idx + 1;
if (!line.isEmpty())
processLine(line);
QByteArray line = cs->readBuffer.left(idx).trimmed();
cs->readBuffer.remove(0, idx + 1);
if (line.isEmpty()) continue;
if (m_processing) {
m_pendingRequests.append({sock, line});
continue;
}
m_processing = true;
m_currentSender = sock;
processLine(line);
m_currentSender = nullptr;
m_processing = false;
drainPendingRequests();
}
}
void McpBridge::drainPendingRequests() {
while (!m_pendingRequests.isEmpty()) {
auto req = m_pendingRequests.takeFirst();
if (!findClient(req.socket)) continue; // client disconnected while queued
m_processing = true;
m_currentSender = req.socket;
processLine(req.line);
m_currentSender = nullptr;
m_processing = false;
}
if (consumed > 0)
m_readBuffer.remove(0, consumed);
}
void McpBridge::onDisconnected() {
qDebug() << "[MCP] Client disconnected";
m_client = nullptr;
m_initialized = false;
auto* sock = qobject_cast<QLocalSocket*>(sender());
qDebug() << "[MCP] Client disconnected (" << m_clients.size() - 1 << "remaining)";
// Purge any queued requests from this client
m_pendingRequests.erase(
std::remove_if(m_pendingRequests.begin(), m_pendingRequests.end(),
[sock](const PendingRequest& r) { return r.socket == sock; }),
m_pendingRequests.end());
removeClient(sock);
}
// ════════════════════════════════════════════════════════════════════
@@ -142,18 +212,26 @@ QJsonObject McpBridge::errReply(const QJsonValue& id, int code, const QString& m
}
void McpBridge::sendJson(const QJsonObject& obj) {
if (!m_client) return;
QLocalSocket* target = m_currentSender;
if (!target || !findClient(target)) return;
QByteArray data = QJsonDocument(obj).toJson(QJsonDocument::Compact);
qDebug() << "[MCP] >>" << data.left(200);
data.append('\n');
m_client->write(data);
m_client->flush();
target->write(data);
target->flush();
}
void McpBridge::sendNotification(const QString& method, const QJsonObject& params) {
QJsonObject n{{"jsonrpc", "2.0"}, {"method", method}};
if (!params.isEmpty()) n["params"] = params;
sendJson(n);
QByteArray data = QJsonDocument(n).toJson(QJsonDocument::Compact);
data.append('\n');
for (auto& c : m_clients) {
if (c.initialized) {
c.socket->write(data);
c.socket->flush();
}
}
}
QJsonObject McpBridge::makeTextResult(const QString& text, bool isError) {
@@ -219,7 +297,7 @@ void McpBridge::processLine(const QByteArray& line) {
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject&) {
m_initialized = true;
if (auto* cs = findClient(m_currentSender)) cs->initialized = true;
QJsonObject caps;
caps["tools"] = QJsonObject{{"listChanged", false}};
@@ -352,6 +430,21 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
}}
});
// 3b. source.modules
tools.append(QJsonObject{
{"name", "source.modules"},
{"description", "List modules for the current data source. Returns name, base (hex), and size for each module. "
"Only available when the provider reports module info (e.g. after attaching to a process). "
"Use these names in baseAddressFormula for tree base, e.g. '<Module.exe> + 0x1000'."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}}
}}
}}
});
// 4. hex.read
tools.append(QJsonObject{
{"name", "hex.read"},
@@ -474,6 +567,73 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
}}
});
// 10. scanner.scan
tools.append(QJsonObject{
{"name", "scanner.scan"},
{"description", "Run a value scan on the active tab's provider and wait for completion. "
"Use after source.switch (e.g. attach to process). Value type: int8, int16, int32, int64, "
"uint8, uint16, uint32, uint64, float, double. Results appear in the Scanner panel. "
"For value scans (e.g. float 120) prefer scanning readable/writable (data) regions, not executable: "
"set filterWritable: true and filterExecutable: false. "
"Use 'regions' to restrict scan to specific address ranges (intersected with provider regions)."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}},
{"valueType", QJsonObject{{"type", "string"},
{"description", "Value type: float, double, int32, uint32, int64, uint64, int16, uint16, int8, uint8."}}},
{"value", QJsonObject{{"type", "string"},
{"description", "Value to search for (e.g. \"120\" for float 120)."}}},
{"filterExecutable", QJsonObject{{"type", "boolean"},
{"description", "Only scan executable regions (default false). For value scans use false; use writable instead."}}},
{"filterWritable", QJsonObject{{"type", "boolean"},
{"description", "Only scan writable regions (default false). Recommended true for value scans to hit data/heap, not code."}}},
{"regions", QJsonObject{{"type", "array"},
{"description", "Restrict scan to these address ranges. Each element is [startHex, endHex], e.g. [[\"0x10000\",\"0x20000\"],[\"0x50000\",\"0x60000\"]]. Ranges are intersected with the provider's real memory regions."},
{"items", QJsonObject{{"type", "array"}, {"items", QJsonObject{{"type", "string"}}}}}}}
}},
{"required", QJsonArray{"valueType", "value"}}
}}
});
// 10. scanner.scan_pattern
tools.append(QJsonObject{
{"name", "scanner.scan_pattern"},
{"description", "Run a pattern/signature scan on the active tab's provider and wait for completion. "
"Pattern is space-separated hex bytes, e.g. '00 00 20 42 00 00 20 42'. Use ?? for wildcards. "
"Results appear in the Scanner panel. Uses the same region list as value scans. "
"Use 'regions' to restrict scan to specific address ranges (intersected with provider regions)."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}},
{"pattern", QJsonObject{{"type", "string"},
{"description", "Hex pattern, e.g. '00 00 20 42 00 00 20 42 00 00 00 00 00 00 00 00'. Use ?? for wildcard bytes."}}},
{"filterExecutable", QJsonObject{{"type", "boolean"},
{"description", "Only scan executable regions (default false)."}}},
{"filterWritable", QJsonObject{{"type", "boolean"},
{"description", "Only scan writable regions (default false)."}}},
{"regions", QJsonObject{{"type", "array"},
{"description", "Restrict scan to these address ranges. Each element is [startHex, endHex], e.g. [[\"0x10000\",\"0x20000\"],[\"0x50000\",\"0x60000\"]]. Ranges are intersected with the provider's real memory regions."},
{"items", QJsonObject{{"type", "array"}, {"items", QJsonObject{{"type", "string"}}}}}}}
}},
{"required", QJsonArray{"pattern"}}
}}
});
// 11. mcp.reconnect
tools.append(QJsonObject{
{"name", "mcp.reconnect"},
{"description", "Disconnect the current MCP client so it can reconnect to Reclass (e.g. after Reclass was restarted or to reset connection state). "
"The client process will exit; your IDE may restart it automatically, reconnecting to Reclass like at startup."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{}}
}}
});
// process.info
tools.append(QJsonObject{
@@ -509,12 +669,16 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
if (toolName == "project.state") result = toolProjectState(args);
else if (toolName == "tree.apply") result = toolTreeApply(args);
else if (toolName == "source.switch") result = toolSourceSwitch(args);
else if (toolName == "source.modules") result = toolSourceModules(args);
else if (toolName == "hex.read") result = toolHexRead(args);
else if (toolName == "hex.write") result = toolHexWrite(args);
else if (toolName == "status.set") result = toolStatusSet(args);
else if (toolName == "ui.action") result = toolUiAction(args);
else if (toolName == "tree.search") result = toolTreeSearch(args);
else if (toolName == "node.history") result = toolNodeHistory(args);
else if (toolName == "scanner.scan") result = toolScannerScan(args);
else if (toolName == "scanner.scan_pattern") result = toolScannerScanPattern(args);
else if (toolName == "mcp.reconnect") result = toolReconnect(args);
else if (toolName == "process.info") result = toolProcessInfo(args);
else return errReply(id, -32601, "Unknown tool: " + toolName);
@@ -550,7 +714,7 @@ MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args, int* resolv
// 1) Explicit tab index from args
if (args.contains("tabIndex")) {
int idx = args.value("tabIndex").toInt();
int idx = (int)parseInteger(args.value("tabIndex"));
auto* t = m_mainWindow->tabByIndex(idx);
if (t) { if (resolvedIndex) *resolvedIndex = idx; return t; }
}
@@ -590,16 +754,18 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
auto* ctrl = tab->ctrl;
const auto& tree = doc->tree;
int maxDepth = args.value("depth").toInt(1);
int maxDepth = (int)parseInteger(args.value("depth"), 1);
bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true;
bool includeMembers = args.value("includeMembers").toBool(false);
int limit = qBound(1, args.value("limit").toInt(50), 500);
int offset = qMax(0, args.value("offset").toInt(0));
int limit = qBound(1, (int)parseInteger(args.value("limit"), 50), 500);
int offset = qMax(0, (int)parseInteger(args.value("offset"), 0));
QString parentIdStr = args.value("parentId").toString();
uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();
QJsonObject state;
state["baseAddress"] = "0x" + QString::number(tree.baseAddress, 16).toUpper();
if (!tree.baseAddressFormula.isEmpty())
state["baseAddressFormula"] = tree.baseAddressFormula;
state["viewRootId"] = QString::number(ctrl->viewRootId());
state["nodeCount"] = tree.nodes.size();
@@ -715,6 +881,8 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
QJsonObject treeObj;
treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
if (!tree.baseAddressFormula.isEmpty())
treeObj["baseAddressFormula"] = tree.baseAddressFormula;
treeObj["nextId"] = QString::number(tree.m_nextId);
treeObj["nodes"] = nodeArr;
treeObj["returned"] = emitted;
@@ -791,12 +959,12 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
skippedOps.append(QStringLiteral("op[%1]: parentId '%2' not found").arg(i).arg(pid));
continue;
}
n.offset = op.value("offset").toInt(0);
n.offset = (int)parseInteger(op.value("offset"), 0);
n.structTypeName = op.value("structTypeName").toString();
n.classKeyword = op.value("classKeyword").toString();
n.strLen = qBound(1, op.value("strLen").toInt(64), 1000000);
n.strLen = qBound(1, (int)parseInteger(op.value("strLen"), 64), 1000000);
n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
n.arrayLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
n.arrayLen = qBound(1, (int)parseInteger(op.value("arrayLen"), 1), 1000000);
bool refOk;
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders, &refOk);
if (!refOk) {
@@ -868,7 +1036,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) {
int newOff = op.value("offset").toInt();
int newOff = (int)parseInteger(op.value("offset"));
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeOffset{tree.nodes[idx].id, tree.nodes[idx].offset, newOff}));
applied++;
@@ -928,7 +1096,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) {
NodeKind newElemKind = kindFromString(op.value("elementKind").toString());
int newLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
int newLen = qBound(1, (int)parseInteger(op.value("arrayLen"), 1), 1000000);
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeArrayMeta{tree.nodes[idx].id,
tree.nodes[idx].elementKind, newElemKind,
@@ -997,7 +1165,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
auto* doc = tab->doc;
if (args.contains("sourceIndex")) {
int idx = args.value("sourceIndex").toInt();
int idx = (int)parseInteger(args.value("sourceIndex"));
const auto& sources = ctrl->savedSources();
if (idx < 0 || idx >= sources.size())
return makeTextResult("Source index out of range: " + QString::number(idx), true);
@@ -1014,11 +1182,17 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
}
if (args.contains("pid")) {
uint32_t pid = (uint32_t)args.value("pid").toInt();
uint32_t pid = (uint32_t)parseInteger(args.value("pid"));
QString name = args.value("processName").toString();
if (name.isEmpty()) name = QString("PID %1").arg(pid);
QString target = QString("%1:%2").arg(pid).arg(name);
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
// attachViaPlugin does not set tree.baseAddress; set it from the new provider (like selectSource does).
if (doc->provider && doc->provider->base() != 0) {
doc->tree.baseAddress = doc->provider->base();
doc->tree.baseAddressFormula.clear();
ctrl->refresh();
}
return makeTextResult("Attached to process " + name + " (PID " + QString::number(pid) + ")");
}
@@ -1032,6 +1206,54 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
return makeTextResult("Provide sourceIndex, filePath, or pid", true);
}
// ════════════════════════════════════════════════════════════════════
// TOOL: source.modules
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolSourceModules(const QJsonObject& args) {
auto* tab = resolveTab(args);
if (!tab) return makeTextResult("No active tab", true);
auto* prov = tab->doc->provider.get();
if (!prov) return makeTextResult("No data source attached", true);
QVector<MemoryRegion> regions = prov->enumerateRegions();
// Build unique modules: name -> { minBase, maxEnd }
QHash<QString, QPair<uint64_t, uint64_t>> moduleMap;
for (const auto& r : regions) {
if (r.moduleName.isEmpty()) continue;
uint64_t end = r.base + r.size;
auto it = moduleMap.find(r.moduleName);
if (it == moduleMap.end()) {
moduleMap[r.moduleName] = qMakePair(r.base, end);
} else {
it->first = qMin(it->first, r.base);
it->second = qMax(it->second, end);
}
}
QJsonArray arr;
QStringList names = moduleMap.keys();
std::sort(names.begin(), names.end(), [](const QString& a, const QString& b) {
return QString::compare(a, b, Qt::CaseInsensitive) < 0;
});
for (const QString& name : names) {
const auto& p = moduleMap[name];
uint64_t base = p.first;
uint64_t size = p.second - p.first;
arr.append(QJsonObject{
{"name", name},
{"base", "0x" + QString::number(base, 16).toUpper()},
{"size", QJsonValue(static_cast<qint64>(size))}
});
}
QJsonObject out;
out["modules"] = arr;
out["count"] = arr.size();
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
}
// ════════════════════════════════════════════════════════════════════
// TOOL: hex.read
// ════════════════════════════════════════════════════════════════════
@@ -1043,10 +1265,11 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
auto* prov = tab->doc->provider.get();
if (!prov) return makeTextResult("No provider", true);
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
int length = qMin(args.value("length").toInt(64), 4096);
int64_t offset = parseInteger(args.value("offset"));
int length = qBound(1, (int)parseInteger(args.value("length"), 64), 4096);
bool baseRel = args.value("baseRelative").toBool();
if (!args.value("baseRelative").toBool())
if (baseRel)
offset += (int64_t)tab->doc->tree.baseAddress;
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
@@ -1125,10 +1348,10 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
auto* doc = tab->doc;
auto* prov = doc->provider.get();
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
int64_t offset = parseInteger(args.value("offset"));
QString hexStr = args.value("hexBytes").toString().remove(' ');
if (!args.value("baseRelative").toBool())
if (args.value("baseRelative").toBool())
offset += (int64_t)doc->tree.baseAddress;
if (hexStr.size() % 2 != 0)
@@ -1312,7 +1535,7 @@ QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) {
const auto& tree = tab->doc->tree;
QString query = args.value("query").toString();
QString kindFilter = args.value("kindFilter").toString();
int limit = qBound(1, args.value("limit").toInt(20), 100);
int limit = qBound(1, (int)parseInteger(args.value("limit"), 20), 100);
if (query.isEmpty() && kindFilter.isEmpty())
return makeTextResult("Provide 'query' (name substring) and/or 'kindFilter' (e.g. 'Struct')", true);
@@ -1402,6 +1625,168 @@ QJsonObject McpBridge::toolNodeHistory(const QJsonObject& args) {
QJsonDocument(result).toJson(QJsonDocument::Compact)));
}
// TOOL: scanner.scan
// ════════════════════════════════════════════════════════════════════
static ValueType valueTypeFromString(const QString& s) {
QString lower = s.trimmed().toLower();
if (lower == QStringLiteral("int8")) return ValueType::Int8;
if (lower == QStringLiteral("int16")) return ValueType::Int16;
if (lower == QStringLiteral("int32")) return ValueType::Int32;
if (lower == QStringLiteral("int64")) return ValueType::Int64;
if (lower == QStringLiteral("uint8")) return ValueType::UInt8;
if (lower == QStringLiteral("uint16")) return ValueType::UInt16;
if (lower == QStringLiteral("uint32")) return ValueType::UInt32;
if (lower == QStringLiteral("uint64")) return ValueType::UInt64;
if (lower == QStringLiteral("float")) return ValueType::Float;
if (lower == QStringLiteral("double")) return ValueType::Double;
return ValueType::Float; // default
}
static QVector<AddressRange> parseRegionsArg(const QJsonObject& args, QString* errOut = nullptr) {
QVector<AddressRange> out;
QJsonArray arr = args.value("regions").toArray();
if (arr.isEmpty()) return out;
out.reserve(arr.size());
for (int i = 0; i < arr.size(); i++) {
QJsonArray pair = arr[i].toArray();
if (pair.size() != 2) {
if (errOut) *errOut = QStringLiteral("regions[%1]: expected [startHex, endHex]").arg(i);
return {};
}
bool ok1 = false, ok2 = false;
uint64_t start = pair[0].toString().toULongLong(&ok1, 0);
uint64_t end = pair[1].toString().toULongLong(&ok2, 0);
if (!ok1 || !ok2) {
if (errOut) *errOut = QStringLiteral("regions[%1]: invalid hex address").arg(i);
return {};
}
if (end <= start) {
if (errOut) *errOut = QStringLiteral("regions[%1]: end must be > start").arg(i);
return {};
}
out.append({start, end});
}
return out;
}
QJsonObject McpBridge::toolScannerScan(const QJsonObject& args) {
auto* tab = resolveTab(args);
if (!tab) return makeTextResult("No active tab", true);
ScannerPanel* panel = m_mainWindow->m_scannerPanel;
if (!panel) return makeTextResult("Scanner panel not available", true);
QString valueTypeStr = args.value("valueType").toString();
QString value = args.value("value").toString();
bool filterExec = args.value("filterExecutable").toBool();
bool filterWrite = args.value("filterWritable").toBool();
if (value.isEmpty())
return makeTextResult("Missing 'value' (e.g. \"120\")", true);
QString regErr;
auto constrainRegions = parseRegionsArg(args, &regErr);
if (!regErr.isEmpty())
return makeTextResult(regErr, true);
ValueType vt = valueTypeFromString(valueTypeStr);
QVector<ScanResult> results = panel->runValueScanAndWait(vt, value, filterExec, filterWrite, constrainRegions);
QString msg = QStringLiteral("Scan (%1 = %2): %3 result(s).")
.arg(valueTypeStr.isEmpty() ? QStringLiteral("float") : valueTypeStr)
.arg(value)
.arg(results.size());
if (!constrainRegions.isEmpty()) {
uint64_t totalConstrained = 0;
for (const auto& r : constrainRegions) totalConstrained += r.end - r.start;
msg += QStringLiteral("\nRegion constraint: %1 range(s), %2 bytes total requested.")
.arg(constrainRegions.size()).arg(totalConstrained);
}
const int showAddrs = 15;
if (!results.isEmpty()) {
msg += QStringLiteral("\nFirst addresses:");
for (int i = 0; i < qMin(results.size(), showAddrs); i++) {
msg += QStringLiteral("\n 0x%1").arg(results[i].address, 16, 16, QChar('0'));
if (!results[i].regionModule.isEmpty())
msg += QStringLiteral(" (%1)").arg(results[i].regionModule);
}
if (results.size() > showAddrs)
msg += QStringLiteral("\n ... and %1 more").arg(results.size() - showAddrs);
}
return makeTextResult(msg);
}
// ════════════════════════════════════════════════════════════════════
// TOOL: scanner.scan_pattern
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolScannerScanPattern(const QJsonObject& args) {
auto* tab = resolveTab(args);
if (!tab) return makeTextResult("No active tab", true);
ScannerPanel* panel = m_mainWindow->m_scannerPanel;
if (!panel) return makeTextResult("Scanner panel not available", true);
QString pattern = args.value("pattern").toString().trimmed();
bool filterExec = args.value("filterExecutable").toBool();
bool filterWrite = args.value("filterWritable").toBool();
if (pattern.isEmpty())
return makeTextResult("Missing 'pattern' (e.g. \"00 00 20 42 00 00 20 42\")", true);
QString regErr;
auto constrainRegions = parseRegionsArg(args, &regErr);
if (!regErr.isEmpty())
return makeTextResult(regErr, true);
// Use the resolved tab's provider so the scan runs on the same tab we attached to (source_switch).
// If we used the panel's default getter we'd get the *active* tab's provider, which may be different.
std::shared_ptr<rcx::Provider> provider = (tab->doc && tab->doc->provider) ? tab->doc->provider : nullptr;
if (!provider) {
return makeTextResult("No provider on this tab — the scan did not run. Use source_switch to attach to a process (or open a file), then run the pattern scan again. If you already ran source_switch, ensure the tab that was switched is the one used (e.g. pass tabIndex: 0 for the first tab).", true);
}
QVector<ScanResult> results = panel->runPatternScanAndWait(provider, pattern, filterExec, filterWrite, constrainRegions);
QString msg = QStringLiteral("Pattern scan (%1): %2 result(s).")
.arg(pattern)
.arg(results.size());
if (!constrainRegions.isEmpty()) {
uint64_t totalConstrained = 0;
for (const auto& r : constrainRegions) totalConstrained += r.end - r.start;
msg += QStringLiteral("\nRegion constraint: %1 range(s), %2 bytes total requested.")
.arg(constrainRegions.size()).arg(totalConstrained);
}
const int showAddrs = 15;
if (!results.isEmpty()) {
msg += QStringLiteral("\nFirst addresses:");
for (int i = 0; i < qMin(results.size(), showAddrs); i++) {
msg += QStringLiteral("\n 0x%1").arg(results[i].address, 16, 16, QChar('0'));
if (!results[i].regionModule.isEmpty())
msg += QStringLiteral(" (%1)").arg(results[i].regionModule);
}
if (results.size() > showAddrs)
msg += QStringLiteral("\n ... and %1 more").arg(results.size() - showAddrs);
}
return makeTextResult(msg);
}
// ════════════════════════════════════════════════════════════════════
// TOOL: mcp.reconnect
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolReconnect(const QJsonObject&) {
QLocalSocket* sock = m_currentSender;
if (!sock)
return makeTextResult("No client connected.", true);
// Disconnect after this response is sent so the client receives the result
QTimer::singleShot(0, this, [this, sock]() {
if (findClient(sock))
sock->disconnectFromServer();
});
return makeTextResult("Disconnected. The MCP client will exit; your IDE may restart it and reconnect to Reclass.");
}
// ════════════════════════════════════════════════════════════════════
// TOOL: process.info — PEB address + TEB enumeration
@@ -1440,12 +1825,13 @@ QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
// ════════════════════════════════════════════════════════════════════
void McpBridge::notifyTreeChanged() {
if (!m_client || !m_initialized) return;
m_notifyTimer->start(); // debounce 100ms
if (m_clients.isEmpty()) return;
sendNotification("notifications/resources/updated",
QJsonObject{{"uri", "project://tree"}});
}
void McpBridge::notifyDataChanged() {
if (!m_client || !m_initialized) return;
if (m_clients.isEmpty()) return;
sendNotification("notifications/resources/updated",
QJsonObject{{"uri", "project://data"}});
}

View File

@@ -29,14 +29,32 @@ public:
void notifyDataChanged();
private:
struct ClientState {
QLocalSocket* socket = nullptr;
QByteArray readBuffer;
bool initialized = false;
};
MainWindow* m_mainWindow;
QLocalServer* m_server = nullptr;
QLocalSocket* m_client = nullptr; // single client for v1
QByteArray m_readBuffer;
bool m_initialized = false;
QVector<ClientState> m_clients;
QLocalSocket* m_currentSender = nullptr; // set during request processing
bool m_slowMode = false;
QTimer* m_notifyTimer = nullptr;
// Serial request queue. Some tool calls (scanner, tree.apply) spin nested
// event loops which would let another client's readyRead interleave and
// clobber m_currentSender. Simplest fix without refactoring those tools:
// queue incoming lines while a request is in flight, drain after.
bool m_processing = false;
struct PendingRequest { QLocalSocket* socket; QByteArray line; };
QVector<PendingRequest> m_pendingRequests;
ClientState* findClient(QLocalSocket* sock);
void removeClient(QLocalSocket* sock);
void drainPendingRequests();
// JSON-RPC plumbing
void onNewConnection();
void onReadyRead();
@@ -56,12 +74,16 @@ private:
QJsonObject toolProjectState(const QJsonObject& args);
QJsonObject toolTreeApply(const QJsonObject& args);
QJsonObject toolSourceSwitch(const QJsonObject& args);
QJsonObject toolSourceModules(const QJsonObject& args);
QJsonObject toolHexRead(const QJsonObject& args);
QJsonObject toolHexWrite(const QJsonObject& args);
QJsonObject toolStatusSet(const QJsonObject& args);
QJsonObject toolUiAction(const QJsonObject& args);
QJsonObject toolTreeSearch(const QJsonObject& args);
QJsonObject toolNodeHistory(const QJsonObject& args);
QJsonObject toolScannerScan(const QJsonObject& args);
QJsonObject toolScannerScanPattern(const QJsonObject& args);
QJsonObject toolReconnect(const QJsonObject& args);
QJsonObject toolProcessInfo(const QJsonObject& args);
// Helpers

View File

@@ -207,6 +207,16 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
}
void OptionsDialog::selectPage(int index) {
for (auto it = m_itemPageIndex.begin(); it != m_itemPageIndex.end(); ++it) {
if (it.value() == index) {
m_tree->setCurrentItem(it.key());
m_pages->setCurrentIndex(index);
break;
}
}
}
OptionsResult OptionsDialog::result() const {
OptionsResult r;
r.themeIndex = m_themeCombo->currentIndex();

View File

@@ -27,6 +27,7 @@ public:
explicit OptionsDialog(const OptionsResult& current, QWidget* parent = nullptr);
OptionsResult result() const;
void selectPage(int index);
private:
void filterTree(const QString& text);

View File

@@ -1,5 +1,8 @@
#include "providerregistry.h"
#include <QDebug>
#include <QMenu>
#include <QIcon>
#include <QHash>
ProviderRegistry& ProviderRegistry::instance() {
static ProviderRegistry s_instance;
@@ -56,3 +59,57 @@ const ProviderRegistry::ProviderInfo* ProviderRegistry::findProvider(const QStri
void ProviderRegistry::clear() {
m_providers.clear();
}
void ProviderRegistry::populateSourceMenu(QMenu* menu,
const QVector<SavedSourceDisplay>& savedSources)
{
static const QHash<QString, QString> s_providerIcons = {
{QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")},
{QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")},
{QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")},
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
};
// File source
auto* fileAct = menu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")),
QStringLiteral("File"));
fileAct->setIconVisibleInMenu(true);
fileAct->setData(QStringLiteral("File"));
// Registered providers
const auto& providers = instance().providers();
for (const auto& prov : providers) {
auto it = s_providerIcons.constFind(prov.identifier);
QIcon icon(it != s_providerIcons.constEnd() ? *it
: QStringLiteral(":/vsicons/extensions.svg"));
QString label = prov.dllFileName.isEmpty()
? prov.name
: QStringLiteral("%1 (%2)").arg(prov.name, prov.dllFileName);
auto* act = menu->addAction(icon, label);
act->setIconVisibleInMenu(true);
act->setData(prov.name); // routing key for selectSource()
// Plugin-specific actions (e.g. "Unload Driver" when loaded)
if (prov.plugin)
prov.plugin->populatePluginMenu(menu);
}
// Saved sources
if (!savedSources.isEmpty()) {
menu->addSeparator();
for (int i = 0; i < savedSources.size(); i++) {
auto* act = menu->addAction(savedSources[i].text);
act->setCheckable(true);
act->setChecked(savedSources[i].active);
act->setData(QStringLiteral("#saved:%1").arg(i));
}
menu->addSeparator();
auto* clearAct = menu->addAction(
QIcon(QStringLiteral(":/vsicons/clear-all.svg")),
QStringLiteral("Clear All"));
clearAct->setIconVisibleInMenu(true);
clearAct->setData(QStringLiteral("#clear"));
}
}

View File

@@ -7,6 +7,13 @@
// Forward declarations
namespace rcx { class Provider; }
class QWidget;
class QMenu;
// Lightweight struct for saved source display in menus
struct SavedSourceDisplay {
QString text;
bool active = false;
};
/**
* Global registry for data source providers
@@ -56,7 +63,13 @@ public:
// Clear all providers
void clear();
// Populate a QMenu with source items (File, providers with icons/dll names,
// plugin actions, saved sources). Used by both the main window Data Source
// menu and the RcxEditor inline source picker.
static void populateSourceMenu(QMenu* menu,
const QVector<SavedSourceDisplay>& savedSources = {});
private:
ProviderRegistry() = default;
QList<ProviderInfo> m_providers;

View File

@@ -16,6 +16,13 @@ struct MemoryRegion {
QString moduleName;
};
struct VtopResult {
uint64_t physical = 0;
uint64_t pml4e = 0, pdpte = 0, pde = 0, pte = 0;
uint8_t pageSize = 0; // 0=4KB, 1=2MB, 2=1GB
bool valid = false;
};
class Provider {
public:
virtual ~Provider() = default;
@@ -80,6 +87,19 @@ public:
struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; };
virtual QVector<ThreadInfo> tebs() const { return {}; }
// --- Kernel paging capabilities (override in kernel providers) ---
virtual bool hasKernelPaging() const { return false; }
virtual uint64_t getCr3() const { return 0; }
virtual VtopResult translateAddress(uint64_t va) const {
Q_UNUSED(va); return {};
}
virtual QVector<uint64_t> readPageTable(uint64_t physAddr,
int startIdx = 0,
int count = 512) const {
Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count);
return {};
}
// --- Derived convenience (non-virtual, never override) ---
bool isValid() const { return size() > 0; }

View File

@@ -473,14 +473,14 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
<< " filterExec:" << req.filterExecutable
<< " filterWrite:" << req.filterWritable;
// Fallback for providers that don't enumerate regions (file/buffer)
// Fallback for providers that don't enumerate regions (file/buffer/syscall without modules)
if (regions.isEmpty()) {
MemoryRegion fallback;
fallback.base = 0;
fallback.size = (uint64_t)prov->size();
fallback.readable = true;
fallback.writable = true;
fallback.executable = false;
fallback.executable = true; // unknown; include so filters don't exclude the only region
regions.append(fallback);
}
@@ -492,6 +492,41 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) &&
req.endAddress > req.startAddress;
// If constrainRegions specified, intersect with provider regions
if (!req.constrainRegions.isEmpty()) {
// Sort and merge overlapping/adjacent constraints to avoid duplicate sub-regions
auto constraints = req.constrainRegions;
std::sort(constraints.begin(), constraints.end(),
[](const AddressRange& a, const AddressRange& b) { return a.start < b.start; });
QVector<AddressRange> merged;
for (const auto& c : constraints) {
if (c.end <= c.start) continue; // skip degenerate ranges
if (!merged.isEmpty() && c.start <= merged.last().end)
merged.last().end = qMax(merged.last().end, c.end);
else
merged.append(c);
}
QVector<MemoryRegion> clipped;
for (const auto& region : regions) {
uint64_t rEnd = region.base + region.size;
for (const auto& c : merged) {
if (c.end <= region.base || c.start >= rEnd) continue;
uint64_t iStart = qMax(region.base, c.start);
uint64_t iEnd = qMin(rEnd, c.end);
if (iEnd <= iStart) continue;
MemoryRegion sub = region;
sub.base = iStart;
sub.size = iEnd - iStart;
clipped.append(sub);
}
}
regions = std::move(clipped);
qDebug() << "[scan] constrained to" << regions.size() << "sub-regions from"
<< req.constrainRegions.size() << "address ranges ("
<< merged.size() << "after merge)";
}
// Pre-compute total bytes for progress
uint64_t totalBytes = 0;
for (const auto& r : regions) {
@@ -515,7 +550,8 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
constexpr int kChunk = 256 * 1024;
for (const auto& region : regions) {
for (int regionIndex = 0; regionIndex < regions.size(); ++regionIndex) {
const auto& region = regions[regionIndex];
if (m_abort.load()) break;
if (req.filterExecutable && !region.executable) continue;
@@ -552,6 +588,8 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
if (!prov->read(regStart + off, chunk.data(), readLen)) {
// Skip unreadable chunk
qDebug() << "[scan] read failed region" << regionIndex << "addr" << Qt::showbase << Qt::hex
<< (region.base + off) << "base" << region.base << "off" << off << "len" << readLen << Qt::dec;
off += readLen;
scannedBytes += readLen;
continue;
@@ -594,9 +632,12 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
}
}
// Advance with overlap to catch patterns that straddle chunks
// Advance with overlap to catch patterns that straddle chunks.
// Skip overlap on the final chunk -- nothing follows to overlap into.
uint64_t advance;
if (readLen > overlap)
if ((uint64_t)readLen >= remaining)
advance = remaining; // last chunk, no overlap needed
else if (readLen > overlap)
advance = (uint64_t)(readLen - overlap);
else
advance = 1; // prevent infinite loop on tiny regions

View File

@@ -34,6 +34,11 @@ enum class ScanCondition {
// ── Scan request / result ──
struct AddressRange {
uint64_t start = 0;
uint64_t end = 0; // exclusive
};
struct ScanRequest {
QByteArray pattern; // literal bytes to match (empty for UnknownValue)
QByteArray mask; // 0xFF = must match, 0x00 = wildcard
@@ -49,6 +54,9 @@ struct ScanRequest {
uint64_t startAddress = 0; // 0 = no limit (scan all regions)
uint64_t endAddress = 0; // 0 = no limit (scan all regions)
// If non-empty, only scan within these address ranges (intersected with provider regions).
QVector<AddressRange> constrainRegions;
};
struct ScanResult {

View File

@@ -10,6 +10,7 @@
#include <QApplication>
#include <QMenu>
#include <QPainter>
#include <QEventLoop>
namespace rcx {
@@ -418,6 +419,98 @@ ScanRequest ScannerPanel::buildRequest() {
return req;
}
QVector<ScanResult> ScannerPanel::runValueScanAndWait(ValueType valueType, const QString& value,
bool filterExecutable, bool filterWritable,
const QVector<AddressRange>& constrainRegions) {
QVector<ScanResult> results;
QString err;
ScanRequest req;
if (!serializeValue(valueType, value, req.pattern, req.mask, &err)) {
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
return results;
}
req.alignment = naturalAlignment(valueType);
req.filterExecutable = filterExecutable;
req.filterWritable = filterWritable;
req.constrainRegions = constrainRegions;
auto provider = m_providerGetter ? m_providerGetter() : nullptr;
if (!provider) {
m_statusLabel->setText(QStringLiteral("No provider (attach to a process or open a file first)"));
return results;
}
if (m_engine->isRunning()) {
m_statusLabel->setText(QStringLiteral("Scan already in progress"));
return results;
}
m_lastScanMode = 1;
m_lastValueType = valueType;
m_lastPattern = req.pattern;
m_progressBar->setValue(0);
m_progressBar->show();
m_statusLabel->setText(QStringLiteral("Scanning..."));
QEventLoop loop;
connect(m_engine, &ScanEngine::finished, this, [&results, &loop](const QVector<ScanResult>& r) {
results = r;
loop.quit();
}, Qt::SingleShotConnection);
m_engine->start(provider, req);
loop.exec();
return results;
}
QVector<ScanResult> ScannerPanel::runPatternScanAndWait(const QString& pattern,
bool filterExecutable, bool filterWritable,
const QVector<AddressRange>& constrainRegions) {
auto provider = m_providerGetter ? m_providerGetter() : nullptr;
return runPatternScanAndWait(provider, pattern, filterExecutable, filterWritable, constrainRegions);
}
QVector<ScanResult> ScannerPanel::runPatternScanAndWait(std::shared_ptr<Provider> provider,
const QString& pattern,
bool filterExecutable, bool filterWritable,
const QVector<AddressRange>& constrainRegions) {
QVector<ScanResult> results;
QString err;
ScanRequest req;
if (!parseSignature(pattern, req.pattern, req.mask, &err)) {
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
return results;
}
req.alignment = 1;
req.filterExecutable = filterExecutable;
req.filterWritable = filterWritable;
req.constrainRegions = constrainRegions;
if (!provider) {
m_statusLabel->setText(QStringLiteral("No provider (attach to a process or open a file first)"));
return results;
}
if (m_engine->isRunning()) {
m_statusLabel->setText(QStringLiteral("Scan already in progress"));
return results;
}
m_lastScanMode = 0;
m_lastPattern = req.pattern;
m_progressBar->setValue(0);
m_progressBar->show();
m_statusLabel->setText(QStringLiteral("Scanning..."));
QEventLoop loop;
connect(m_engine, &ScanEngine::finished, this, [&results, &loop](const QVector<ScanResult>& r) {
results = r;
loop.quit();
}, Qt::SingleShotConnection);
m_engine->start(provider, req);
loop.exec();
return results;
}
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
m_scanBtn->setText(QStringLiteral("Scan"));
m_progressBar->hide();

View File

@@ -60,6 +60,21 @@ public:
QLabel* condLabel() const { return m_condLabel; }
QCheckBox* structOnlyCheck() const { return m_structOnlyCheck; }
/** Run a value scan and block until done. For MCP / automation. Returns results; updates panel table. */
QVector<ScanResult> runValueScanAndWait(ValueType valueType, const QString& value,
bool filterExecutable = false, bool filterWritable = false,
const QVector<AddressRange>& constrainRegions = {});
/** Run a pattern/signature scan and block until done. Pattern: space-separated hex bytes, e.g. "00 00 20 42 ?? ??". */
QVector<ScanResult> runPatternScanAndWait(const QString& pattern,
bool filterExecutable = false, bool filterWritable = false,
const QVector<AddressRange>& constrainRegions = {});
/** Run pattern scan using the given provider (for MCP: use tab's provider so scan runs on the right tab). */
QVector<ScanResult> runPatternScanAndWait(std::shared_ptr<Provider> provider, const QString& pattern,
bool filterExecutable = false, bool filterWritable = false,
const QVector<AddressRange>& constrainRegions = {});
signals:
void goToAddress(uint64_t address);

View File

@@ -873,6 +873,559 @@ private slots:
QVERIFY2(result.contains("sizeof(Small) == 0x4"),
qPrintable("Expected sizeof(Small) == 0x4:\n" + result));
}
// ═══════════════════════════════════════════════════════════
// ── Rust backend tests ──
// ═══════════════════════════════════════════════════════════
void testRustSimpleStruct() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString result = rcx::renderRust(tree, rootId, nullptr, true);
QVERIFY(result.contains("// Generated by Reclass 2027"));
QVERIFY(result.contains("#[repr(C)]"));
QVERIFY(result.contains("pub struct Player {"));
QVERIFY(result.contains("pub health: i32,"));
QVERIFY(result.contains("pub speed: f32,"));
QVERIFY(result.contains("pub id: u64,"));
QVERIFY(result.contains("// 0x0"));
QVERIFY(result.contains("// 0x4"));
QVERIFY(result.contains("// 0x8"));
QVERIFY(result.contains("core::mem::size_of::<Player>() == 0x10"));
// Without asserts
QString noAsserts = rcx::renderRust(tree, rootId);
QVERIFY(!noAsserts.contains("size_of"));
}
void testRustPadding() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Padded";
root.structTypeName = "Padded";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node f1;
f1.kind = rcx::NodeKind::UInt32;
f1.name = "a";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
rcx::Node f2;
f2.kind = rcx::NodeKind::UInt32;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 8;
tree.addNode(f2);
QString result = rcx::renderRust(tree, rootId);
QVERIFY(result.contains("pub _pad"));
QVERIFY(result.contains("[u8; 0x4]"));
}
void testRustPointers() {
rcx::NodeTree tree;
rcx::Node target;
target.kind = rcx::NodeKind::Struct;
target.name = "Target";
target.structTypeName = "Target";
target.parentId = 0;
target.offset = 0x100;
int ti = tree.addNode(target);
uint64_t targetId = tree.nodes[ti].id;
rcx::Node tf;
tf.kind = rcx::NodeKind::UInt32;
tf.name = "val";
tf.parentId = targetId;
tf.offset = 0;
tree.addNode(tf);
rcx::Node main;
main.kind = rcx::NodeKind::Struct;
main.name = "PtrTest";
main.structTypeName = "PtrTest";
main.parentId = 0;
int mi = tree.addNode(main);
uint64_t mainId = tree.nodes[mi].id;
rcx::Node p1;
p1.kind = rcx::NodeKind::Pointer64;
p1.name = "typed";
p1.parentId = mainId;
p1.offset = 0;
p1.refId = targetId;
tree.addNode(p1);
rcx::Node p2;
p2.kind = rcx::NodeKind::Pointer64;
p2.name = "untyped";
p2.parentId = mainId;
p2.offset = 8;
tree.addNode(p2);
QString result = rcx::renderRust(tree, mainId);
QVERIFY(result.contains("pub typed: *mut Target,"));
QVERIFY(result.contains("pub untyped: *mut core::ffi::c_void,"));
}
void testRustVectors() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Vecs";
root.structTypeName = "Vecs";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node v2;
v2.kind = rcx::NodeKind::Vec2;
v2.name = "pos";
v2.parentId = rootId;
v2.offset = 0;
tree.addNode(v2);
rcx::Node v4;
v4.kind = rcx::NodeKind::Vec4;
v4.name = "color";
v4.parentId = rootId;
v4.offset = 8;
tree.addNode(v4);
QString result = rcx::renderRust(tree, rootId);
QVERIFY(result.contains("pub pos: [f32; 2],"));
QVERIFY(result.contains("pub color: [f32; 4],"));
}
void testRustFuncPtr() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "FP";
root.structTypeName = "FP";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node fp;
fp.kind = rcx::NodeKind::FuncPtr64;
fp.name = "callback";
fp.parentId = rootId;
fp.offset = 0;
tree.addNode(fp);
QString result = rcx::renderRust(tree, rootId);
QVERIFY(result.contains("pub callback: Option<unsafe extern \"C\" fn()>,"));
}
void testRustAll() {
auto tree = makeSimpleStruct();
QString result = rcx::renderRustAll(tree, nullptr, true);
QVERIFY(result.contains("#[repr(C)]"));
QVERIFY(result.contains("pub struct Player {"));
QVERIFY(result.contains("core::mem::size_of::<Player>()"));
}
// ═══════════════════════════════════════════════════════════
// ── #define offsets backend tests ──
// ═══════════════════════════════════════════════════════════
void testDefineSimpleStruct() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString result = rcx::renderDefines(tree, rootId);
QVERIFY(result.contains("#pragma once"));
QVERIFY(result.contains("// Player"));
QVERIFY(result.contains("#define Player_health 0x0"));
QVERIFY(result.contains("#define Player_speed 0x4"));
QVERIFY(result.contains("#define Player_id 0x8"));
}
void testDefineSkipsHex() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "HexTest";
root.structTypeName = "HexTest";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node h;
h.kind = rcx::NodeKind::Hex32;
h.name = "padding";
h.parentId = rootId;
h.offset = 0;
tree.addNode(h);
rcx::Node f;
f.kind = rcx::NodeKind::UInt32;
f.name = "real_field";
f.parentId = rootId;
f.offset = 4;
tree.addNode(f);
QString result = rcx::renderDefines(tree, rootId);
QVERIFY(!result.contains("padding"));
QVERIFY(result.contains("#define HexTest_real_field 0x4"));
}
void testDefineAll() {
auto tree = makeSimpleStruct();
QString result = rcx::renderDefinesAll(tree);
QVERIFY(result.contains("#pragma once"));
QVERIFY(result.contains("#define Player_health 0x0"));
}
// ═══════════════════════════════════════════════════════════
// ── Format dispatch tests ──
// ═══════════════════════════════════════════════════════════
void testCodeFormatDispatch() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString cpp = rcx::renderCode(rcx::CodeFormat::CppHeader, tree, rootId);
QVERIFY(cpp.contains("struct Player"));
QString rust = rcx::renderCode(rcx::CodeFormat::RustStruct, tree, rootId);
QVERIFY(rust.contains("pub struct Player"));
QString defs = rcx::renderCode(rcx::CodeFormat::DefineOffsets, tree, rootId);
QVERIFY(defs.contains("#define Player_health"));
}
void testCodeFormatAllDispatch() {
auto tree = makeSimpleStruct();
QString cpp = rcx::renderCodeAll(rcx::CodeFormat::CppHeader, tree);
QVERIFY(cpp.contains("struct Player"));
QString rust = rcx::renderCodeAll(rcx::CodeFormat::RustStruct, tree);
QVERIFY(rust.contains("pub struct Player"));
QString defs = rcx::renderCodeAll(rcx::CodeFormat::DefineOffsets, tree);
QVERIFY(defs.contains("#define Player_health"));
}
// ═══════════════════════════════════════════════════════════
// ── Scope tests (Current + Deps) ──
// ═══════════════════════════════════════════════════════════
void testTreeScopeIncludesReferencedTypes() {
rcx::NodeTree tree;
// Target struct (referenced by pointer)
rcx::Node target;
target.kind = rcx::NodeKind::Struct;
target.name = "Target";
target.structTypeName = "Target";
target.parentId = 0;
target.offset = 0x100;
int ti = tree.addNode(target);
uint64_t targetId = tree.nodes[ti].id;
rcx::Node tf;
tf.kind = rcx::NodeKind::UInt32;
tf.name = "val";
tf.parentId = targetId;
tf.offset = 0;
tree.addNode(tf);
// Main struct with a pointer to Target
rcx::Node main;
main.kind = rcx::NodeKind::Struct;
main.name = "Main";
main.structTypeName = "Main";
main.parentId = 0;
int mi = tree.addNode(main);
uint64_t mainId = tree.nodes[mi].id;
rcx::Node ptr;
ptr.kind = rcx::NodeKind::Pointer64;
ptr.name = "pTarget";
ptr.parentId = mainId;
ptr.offset = 0;
ptr.refId = targetId;
tree.addNode(ptr);
// "Current" scope: only Main, no Target definition
QString current = rcx::renderCpp(tree, mainId);
QVERIFY(current.contains("struct Main\n{"));
QVERIFY(!current.contains("struct Target\n{"));
// "Current + Deps" scope: Main AND Target definitions
QString withDeps = rcx::renderCppTree(tree, mainId);
QVERIFY(withDeps.contains("struct Main\n{"));
QVERIFY(withDeps.contains("struct Target\n{"));
// Same for Rust
QString rustDeps = rcx::renderRustTree(tree, mainId);
QVERIFY(rustDeps.contains("pub struct Main {"));
QVERIFY(rustDeps.contains("pub struct Target {"));
// Same for #define
QString defDeps = rcx::renderDefinesTree(tree, mainId);
QVERIFY(defDeps.contains("#define Main_pTarget"));
QVERIFY(defDeps.contains("#define Target_val"));
}
void testTreeScopeDispatch() {
rcx::NodeTree tree;
rcx::Node a;
a.kind = rcx::NodeKind::Struct;
a.name = "A";
a.structTypeName = "A";
a.parentId = 0;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
rcx::Node af;
af.kind = rcx::NodeKind::UInt32;
af.name = "x";
af.parentId = aId;
af.offset = 0;
tree.addNode(af);
// renderCodeTree should work for all formats
QString cpp = rcx::renderCodeTree(rcx::CodeFormat::CppHeader, tree, aId);
QVERIFY(cpp.contains("struct A"));
QString rust = rcx::renderCodeTree(rcx::CodeFormat::RustStruct, tree, aId);
QVERIFY(rust.contains("pub struct A"));
QString defs = rcx::renderCodeTree(rcx::CodeFormat::DefineOffsets, tree, aId);
QVERIFY(defs.contains("#define A_x"));
QString cs = rcx::renderCodeTree(rcx::CodeFormat::CSharpStruct, tree, aId);
QVERIFY(cs.contains("public unsafe struct A"));
QString py = rcx::renderCodeTree(rcx::CodeFormat::PythonCtypes, tree, aId);
QVERIFY(py.contains("class A(ctypes.Structure)"));
}
// ── C# backend ──
void testCSharpSimpleStruct() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString result = rcx::renderCSharp(tree, rootId);
QVERIFY(result.contains("using System.Runtime.InteropServices;"));
QVERIFY(result.contains("[StructLayout(LayoutKind.Explicit, Size = 0x10)]"));
QVERIFY(result.contains("public unsafe struct Player"));
QVERIFY(result.contains("[FieldOffset(0x0)] public int health;"));
QVERIFY(result.contains("[FieldOffset(0x4)] public float speed;"));
QVERIFY(result.contains("[FieldOffset(0x8)] public ulong id;"));
}
void testCSharpPointers() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Foo";
root.structTypeName = "Foo";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node p;
p.kind = rcx::NodeKind::Pointer64;
p.name = "ptr";
p.parentId = rootId;
p.offset = 0;
tree.addNode(p);
QString result = rcx::renderCSharp(tree, rootId);
QVERIFY(result.contains("IntPtr ptr"));
}
void testCSharpAll() {
auto tree = makeSimpleStruct();
QString result = rcx::renderCSharpAll(tree);
QVERIFY(result.contains("public unsafe struct Player"));
QVERIFY(result.contains("[StructLayout("));
}
void testCSharpEnum() {
rcx::NodeTree tree;
rcx::Node e;
e.kind = rcx::NodeKind::Struct;
e.name = "Color";
e.structTypeName = "Color";
e.classKeyword = "enum";
e.parentId = 0;
e.offset = 0;
e.enumMembers = {{"Red", 0}, {"Green", 1}, {"Blue", 2}};
tree.addNode(e);
QString result = rcx::renderCSharpAll(tree);
QVERIFY(result.contains("public enum Color : long"));
QVERIFY(result.contains("Red = 0"));
QVERIFY(result.contains("Green = 1"));
QVERIFY(result.contains("Blue = 2"));
}
void testCSharpVectors() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Xform";
root.structTypeName = "Xform";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node v;
v.kind = rcx::NodeKind::Vec3;
v.name = "position";
v.parentId = rootId;
v.offset = 0;
tree.addNode(v);
QString result = rcx::renderCSharp(tree, rootId);
QVERIFY(result.contains("public fixed float position[3]"));
}
// ── Python ctypes backend ──
void testPythonSimpleStruct() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString result = rcx::renderPython(tree, rootId);
QVERIFY(result.contains("import ctypes"));
QVERIFY(result.contains("class Player(ctypes.Structure)"));
QVERIFY(result.contains("_fields_ = ["));
QVERIFY(result.contains("(\"health\", ctypes.c_int32)"));
QVERIFY(result.contains("(\"speed\", ctypes.c_float)"));
QVERIFY(result.contains("(\"id\", ctypes.c_uint64)"));
}
void testPythonPointers() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Bar";
root.structTypeName = "Bar";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node p;
p.kind = rcx::NodeKind::Pointer64;
p.name = "ptr";
p.parentId = rootId;
p.offset = 0;
tree.addNode(p);
QString result = rcx::renderPython(tree, rootId);
QVERIFY(result.contains("(\"ptr\", ctypes.c_void_p)"));
}
void testPythonTypedPointers() {
rcx::NodeTree tree;
rcx::Node target;
target.kind = rcx::NodeKind::Struct;
target.name = "Target";
target.structTypeName = "Target";
target.parentId = 0;
target.offset = 0;
int ti = tree.addNode(target);
uint64_t targetId = tree.nodes[ti].id;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Holder";
root.structTypeName = "Holder";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node p;
p.kind = rcx::NodeKind::Pointer64;
p.name = "ref";
p.parentId = rootId;
p.offset = 0;
p.refId = targetId;
tree.addNode(p);
QString result = rcx::renderPython(tree, rootId);
QVERIFY(result.contains("ctypes.POINTER(Target)"));
}
void testPythonAll() {
auto tree = makeSimpleStruct();
QString result = rcx::renderPythonAll(tree);
QVERIFY(result.contains("class Player(ctypes.Structure)"));
}
void testPythonEnum() {
rcx::NodeTree tree;
rcx::Node e;
e.kind = rcx::NodeKind::Struct;
e.name = "Status";
e.structTypeName = "Status";
e.classKeyword = "enum";
e.parentId = 0;
e.offset = 0;
e.enumMembers = {{"Active", 1}, {"Inactive", 0}};
tree.addNode(e);
QString result = rcx::renderPythonAll(tree);
QVERIFY(result.contains("class Status:"));
QVERIFY(result.contains("Active = 1"));
QVERIFY(result.contains("Inactive = 0"));
}
void testPythonVectors() {
rcx::NodeTree tree;
rcx::Node root;
root.kind = rcx::NodeKind::Struct;
root.name = "Pos";
root.structTypeName = "Pos";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
rcx::Node v;
v.kind = rcx::NodeKind::Vec4;
v.name = "color";
v.parentId = rootId;
v.offset = 0;
tree.addNode(v);
QString result = rcx::renderPython(tree, rootId);
QVERIFY(result.contains("(\"color\", ctypes.c_float * 4)"));
}
void testCSharpDispatch() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString result = rcx::renderCode(rcx::CodeFormat::CSharpStruct, tree, rootId);
QVERIFY(result.contains("[StructLayout("));
}
void testPythonDispatch() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString result = rcx::renderCode(rcx::CodeFormat::PythonCtypes, tree, rootId);
QVERIFY(result.contains("ctypes.Structure"));
}
};
QTEST_MAIN(TestGenerator)

378
tests/test_mcp.cpp Normal file
View File

@@ -0,0 +1,378 @@
// Test MCP multi-client protocol: connect, initialize, tools/list,
// disconnect one client, notification broadcast, serial requests.
// Uses a MockMcpServer with the same multi-client architecture as McpBridge.
#include <QTest>
#include <QLocalServer>
#include <QLocalSocket>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QElapsedTimer>
#include <QTimer>
// ── Mock server (same pattern as McpBridge multi-client) ──
class MockMcpServer : public QObject {
Q_OBJECT
public:
struct Client { QLocalSocket* socket; QByteArray buf; bool initialized; };
QLocalServer* m_server = nullptr;
QVector<Client> m_clients;
bool start(const QString& name) {
QLocalServer::removeServer(name);
m_server = new QLocalServer(this);
if (!m_server->listen(name)) return false;
connect(m_server, &QLocalServer::newConnection, this, [this]() {
while (auto* s = m_server->nextPendingConnection()) {
m_clients.append({s, {}, false});
connect(s, &QLocalSocket::readyRead, this, [this, s]() { processSocket(s); });
connect(s, &QLocalSocket::disconnected, this, [this, s]() {
for (int i = 0; i < m_clients.size(); i++)
if (m_clients[i].socket == s) { s->deleteLater(); m_clients.removeAt(i); break; }
});
}
});
return true;
}
void stop() {
for (auto& c : m_clients) { c.socket->disconnect(this); c.socket->disconnectFromServer(); c.socket->deleteLater(); }
m_clients.clear();
if (m_server) { m_server->close(); delete m_server; m_server = nullptr; }
}
int clientCount() const { return m_clients.size(); }
int initializedCount() const { int n=0; for (auto& c:m_clients) if(c.initialized) n++; return n; }
void broadcast(const QJsonObject& obj) {
QByteArray data = QJsonDocument(obj).toJson(QJsonDocument::Compact) + '\n';
for (auto& c : m_clients)
if (c.initialized) { c.socket->write(data); c.socket->flush(); }
}
private:
void sendTo(QLocalSocket* s, const QJsonObject& obj) {
s->write(QJsonDocument(obj).toJson(QJsonDocument::Compact) + '\n');
s->flush();
}
void processSocket(QLocalSocket* s) {
Client* cs = nullptr;
for (auto& c : m_clients) if (c.socket == s) { cs = &c; break; }
if (!cs) return;
cs->buf.append(s->readAll());
while (true) {
int idx = cs->buf.indexOf('\n');
if (idx < 0) break;
QByteArray line = cs->buf.left(idx).trimmed();
cs->buf.remove(0, idx + 1);
if (line.isEmpty()) continue;
auto doc = QJsonDocument::fromJson(line);
if (!doc.isObject()) {
sendTo(s, {{"jsonrpc","2.0"},{"id",QJsonValue()},
{"error",QJsonObject{{"code",-32700},{"message","Parse error"}}}});
continue;
}
auto req = doc.object();
QString method = req["method"].toString();
QJsonValue id = req["id"];
if (method.isEmpty()) {
sendTo(s, {{"jsonrpc","2.0"},{"id",id},
{"error",QJsonObject{{"code",-32600},{"message","Missing method"}}}});
} else if (method == "initialize") {
cs->initialized = true;
sendTo(s, {{"jsonrpc","2.0"},{"id",id},{"result",QJsonObject{
{"protocolVersion","2024-11-05"},
{"serverInfo",QJsonObject{{"name","mock-mcp"},{"version","1.0"}}}}}});
} else if (method == "notifications/initialized" || method == "notifications/cancelled") {
// no-op client notifications
} else if (method == "tools/list") {
sendTo(s, {{"jsonrpc","2.0"},{"id",id},{"result",QJsonObject{
{"tools",QJsonArray{QJsonObject{{"name","test.tool"},{"description","A test"}}}}}}});
} else if (method == "tools/call") {
QString toolName = req["params"].toObject()["name"].toString();
if (toolName == "mcp.reconnect") {
sendTo(s, {{"jsonrpc","2.0"},{"id",id},{"result",QJsonObject{
{"content",QJsonArray{QJsonObject{{"type","text"},{"text","Disconnected."}}}}}}});
// Disconnect after response is flushed
QTimer::singleShot(0, this, [this, s]() {
for (auto& cc : m_clients) if (cc.socket == s) { s->disconnectFromServer(); break; }
});
} else if (toolName.isEmpty()) {
sendTo(s, {{"jsonrpc","2.0"},{"id",id},
{"error",QJsonObject{{"code",-32602},{"message","Missing tool name"}}}});
} else {
sendTo(s, {{"jsonrpc","2.0"},{"id",id},
{"error",QJsonObject{{"code",-32601},{"message","Unknown tool"}}}});
}
} else {
sendTo(s, {{"jsonrpc","2.0"},{"id",id},
{"error",QJsonObject{{"code",-32601},{"message","Method not found"}}}});
}
}
}
};
// ── Helpers ──
static QLocalSocket* makeClient(const QString& pipe, QObject* parent) {
auto* s = new QLocalSocket(parent);
s->connectToServer(pipe);
return s->waitForConnected(2000) ? s : nullptr;
}
// Send JSON-RPC and pump the event loop until we get a response line.
static QJsonObject rpc(QLocalSocket* s, const QJsonObject& req, int ms = 3000) {
s->write(QJsonDocument(req).toJson(QJsonDocument::Compact) + '\n');
s->flush();
QByteArray buf;
QElapsedTimer t; t.start();
while (t.elapsed() < ms) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
if (s->bytesAvailable()) buf.append(s->readAll());
int idx = buf.indexOf('\n');
if (idx >= 0) return QJsonDocument::fromJson(buf.left(idx).trimmed()).object();
}
return {};
}
static QJsonObject initRpc(QLocalSocket* s) {
return rpc(s, {{"jsonrpc","2.0"},{"id",1},{"method","initialize"},
{"params",QJsonObject{{"protocolVersion","2024-11-05"},
{"capabilities",QJsonObject{}},
{"clientInfo",QJsonObject{{"name","test"}}}}}});
}
static QVector<QJsonObject> drain(QLocalSocket* s, int ms = 300) {
QVector<QJsonObject> out;
QByteArray buf;
QElapsedTimer t; t.start();
while (t.elapsed() < ms) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 30);
if (s->bytesAvailable()) buf.append(s->readAll());
}
while (true) {
int idx = buf.indexOf('\n');
if (idx < 0) break;
auto line = buf.left(idx).trimmed();
buf.remove(0, idx + 1);
if (!line.isEmpty()) out.append(QJsonDocument::fromJson(line).object());
}
return out;
}
// ── Tests ──
class TestMcp : public QObject {
Q_OBJECT
MockMcpServer* m_srv = nullptr;
static constexpr const char* P = "ReclassMcpTest";
private slots:
void init() { m_srv = new MockMcpServer; QVERIFY(m_srv->start(P)); }
void cleanup() { m_srv->stop(); delete m_srv; m_srv = nullptr; }
void singleClient_initialize() {
auto* c = makeClient(P, this); QVERIFY(c);
auto r = initRpc(c);
QCOMPARE(r["id"].toInt(), 1);
QVERIFY(r.contains("result"));
QCOMPARE(r["result"].toObject()["serverInfo"].toObject()["name"].toString(), QString("mock-mcp"));
QCOMPARE(m_srv->initializedCount(), 1);
c->disconnectFromServer(); delete c;
}
void singleClient_toolsList() {
auto* c = makeClient(P, this); QVERIFY(c);
initRpc(c);
auto r = rpc(c, {{"jsonrpc","2.0"},{"id",2},{"method","tools/list"}});
QCOMPARE(r["id"].toInt(), 2);
QCOMPARE(r["result"].toObject()["tools"].toArray().size(), 1);
c->disconnectFromServer(); delete c;
}
void singleClient_unknownMethod() {
auto* c = makeClient(P, this); QVERIFY(c);
auto r = rpc(c, {{"jsonrpc","2.0"},{"id",1},{"method","bogus"}});
QVERIFY(r.contains("error"));
QCOMPARE(r["error"].toObject()["code"].toInt(), -32601);
c->disconnectFromServer(); delete c;
}
void multiClient_bothInitialize() {
auto* c1 = makeClient(P, this); auto* c2 = makeClient(P, this);
QVERIFY(c1); QVERIFY(c2);
QCoreApplication::processEvents();
QCOMPARE(m_srv->clientCount(), 2);
auto r1 = initRpc(c1); auto r2 = initRpc(c2);
QVERIFY(r1.contains("result"));
QVERIFY(r2.contains("result"));
QCOMPARE(m_srv->initializedCount(), 2);
c1->disconnectFromServer(); c2->disconnectFromServer(); delete c1; delete c2;
}
void multiClient_disconnectOne() {
auto* c1 = makeClient(P, this); auto* c2 = makeClient(P, this);
QVERIFY(c1); QVERIFY(c2);
initRpc(c1); initRpc(c2);
c1->disconnectFromServer(); QTest::qWait(200);
QCOMPARE(m_srv->clientCount(), 1);
auto r = rpc(c2, {{"jsonrpc","2.0"},{"id",5},{"method","tools/list"}});
QCOMPARE(r["id"].toInt(), 5);
QVERIFY(r["result"].toObject()["tools"].toArray().size() > 0);
c2->disconnectFromServer(); delete c1; delete c2;
}
void multiClient_notificationBroadcast() {
auto* c1 = makeClient(P, this);
auto* c2 = makeClient(P, this);
auto* c3 = makeClient(P, this); // not initialized
QVERIFY(c1); QVERIFY(c2); QVERIFY(c3);
initRpc(c1); initRpc(c2);
m_srv->broadcast({{"jsonrpc","2.0"},
{"method","notifications/resources/updated"},
{"params",QJsonObject{{"uri","project://tree"}}}});
auto l1 = drain(c1); auto l2 = drain(c2); auto l3 = drain(c3);
QVERIFY(l1.size() >= 1);
QCOMPARE(l1.last()["method"].toString(), QString("notifications/resources/updated"));
QVERIFY(l2.size() >= 1);
QCOMPARE(l2.last()["method"].toString(), QString("notifications/resources/updated"));
QCOMPARE(l3.size(), 0);
c1->disconnectFromServer(); c2->disconnectFromServer(); c3->disconnectFromServer();
delete c1; delete c2; delete c3;
}
void multiClient_serialRequests() {
auto* c1 = makeClient(P, this); auto* c2 = makeClient(P, this);
QVERIFY(c1); QVERIFY(c2);
initRpc(c1); initRpc(c2);
auto r1 = rpc(c1, {{"jsonrpc","2.0"},{"id",10},{"method","tools/list"}});
auto r2 = rpc(c2, {{"jsonrpc","2.0"},{"id",20},{"method","tools/list"}});
QCOMPARE(r1["id"].toInt(), 10);
QCOMPARE(r2["id"].toInt(), 20);
c1->disconnectFromServer(); c2->disconnectFromServer(); delete c1; delete c2;
}
void allDisconnect_serverSurvives() {
auto* c1 = makeClient(P, this); QVERIFY(c1);
initRpc(c1);
c1->disconnectFromServer(); QTest::qWait(200);
QCOMPARE(m_srv->clientCount(), 0);
auto* c2 = makeClient(P, this); QVERIFY(c2);
auto r = initRpc(c2);
QVERIFY(r.contains("result"));
QCOMPARE(m_srv->clientCount(), 1);
c2->disconnectFromServer(); delete c1; delete c2;
}
void protocol_invalidJson() {
auto* c = makeClient(P, this); QVERIFY(c);
c->write("this is not json\n");
c->flush();
QByteArray buf;
QElapsedTimer t; t.start();
while (t.elapsed() < 2000) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
if (c->bytesAvailable()) buf.append(c->readAll());
if (buf.indexOf('\n') >= 0) break;
}
auto r = QJsonDocument::fromJson(buf.left(buf.indexOf('\n')).trimmed()).object();
QVERIFY(r.contains("error"));
QCOMPARE(r["error"].toObject()["code"].toInt(), -32700);
c->disconnectFromServer(); delete c;
}
void protocol_missingMethod() {
auto* c = makeClient(P, this); QVERIFY(c);
auto r = rpc(c, {{"jsonrpc","2.0"},{"id",1}}); // no "method" key
QVERIFY(r.contains("error"));
QCOMPARE(r["error"].toObject()["code"].toInt(), -32600);
c->disconnectFromServer(); delete c;
}
void protocol_notificationsIgnored() {
// notifications/initialized and notifications/cancelled should not produce a response
auto* c = makeClient(P, this); QVERIFY(c);
initRpc(c);
c->write(QJsonDocument(QJsonObject{{"jsonrpc","2.0"},{"method","notifications/initialized"}}).toJson(QJsonDocument::Compact) + '\n');
c->write(QJsonDocument(QJsonObject{{"jsonrpc","2.0"},{"method","notifications/cancelled"},{"params",QJsonObject{{"requestId",1}}}}).toJson(QJsonDocument::Compact) + '\n');
c->flush();
auto lines = drain(c, 500);
QCOMPARE(lines.size(), 0); // no response for notifications
c->disconnectFromServer(); delete c;
}
void toolsCall_unknownTool() {
auto* c = makeClient(P, this); QVERIFY(c);
initRpc(c);
auto r = rpc(c, {{"jsonrpc","2.0"},{"id",2},{"method","tools/call"},
{"params",QJsonObject{{"name","nonexistent.tool"},{"arguments",QJsonObject{}}}}});
QVERIFY(r.contains("error"));
QCOMPARE(r["error"].toObject()["code"].toInt(), -32601);
c->disconnectFromServer(); delete c;
}
void toolsCall_missingToolName() {
auto* c = makeClient(P, this); QVERIFY(c);
initRpc(c);
auto r = rpc(c, {{"jsonrpc","2.0"},{"id",3},{"method","tools/call"},
{"params",QJsonObject{{"arguments",QJsonObject{}}}}});
QVERIFY(r.contains("error"));
QCOMPARE(r["error"].toObject()["code"].toInt(), -32602);
c->disconnectFromServer(); delete c;
}
void toolsCall_reconnect() {
auto* c = makeClient(P, this); QVERIFY(c);
initRpc(c);
QCOMPARE(m_srv->clientCount(), 1);
// Call mcp.reconnect — should get response then get disconnected
auto r = rpc(c, {{"jsonrpc","2.0"},{"id",7},{"method","tools/call"},
{"params",QJsonObject{{"name","mcp.reconnect"},{"arguments",QJsonObject{}}}}});
QCOMPARE(r["id"].toInt(), 7);
QVERIFY(r.contains("result"));
QVERIFY(r["result"].toObject()["content"].toArray()[0].toObject()["text"]
.toString().contains("Disconnected"));
// Wait for server-side disconnect
QTest::qWait(300);
QCOMPARE(m_srv->clientCount(), 0);
// Reconnect — should work fine
auto* c2 = makeClient(P, this); QVERIFY(c2);
auto r2 = initRpc(c2);
QVERIFY(r2.contains("result"));
QCOMPARE(m_srv->clientCount(), 1);
// Verify the new connection works
auto r3 = rpc(c2, {{"jsonrpc","2.0"},{"id",8},{"method","tools/list"}});
QCOMPARE(r3["id"].toInt(), 8);
QVERIFY(r3["result"].toObject()["tools"].toArray().size() > 0);
c2->disconnectFromServer(); delete c; delete c2;
}
void toolsCall_reconnect_otherClientUnaffected() {
auto* c1 = makeClient(P, this); auto* c2 = makeClient(P, this);
QVERIFY(c1); QVERIFY(c2);
initRpc(c1); initRpc(c2);
QCOMPARE(m_srv->clientCount(), 2);
// c1 calls reconnect — only c1 should disconnect
rpc(c1, {{"jsonrpc","2.0"},{"id",1},{"method","tools/call"},
{"params",QJsonObject{{"name","mcp.reconnect"},{"arguments",QJsonObject{}}}}});
QTest::qWait(300);
QCOMPARE(m_srv->clientCount(), 1);
// c2 still works
auto r = rpc(c2, {{"jsonrpc","2.0"},{"id",2},{"method","tools/list"}});
QCOMPARE(r["id"].toInt(), 2);
QVERIFY(r["result"].toObject()["tools"].toArray().size() > 0);
c2->disconnectFromServer(); delete c1; delete c2;
}
};
QTEST_GUILESS_MAIN(TestMcp)
#include "test_mcp.moc"

View File

@@ -1186,6 +1186,813 @@ private slots:
QCOMPARE(results[0].address, (uint64_t)8);
QCOMPARE(results[3].address, (uint64_t)20);
}
// -- constrainRegions (multi-range intersection) --
void scan_constrainRegions_multipleRanges() {
QByteArray data(32, 0);
data[4] = char(0xBB);
data[12] = char(0xBB);
data[20] = char(0xBB);
data[28] = char(0xBB);
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\xBB", 1);
req.mask = QByteArray("\xFF", 1);
req.constrainRegions = {{0, 8}, {16, 24}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
QCOMPARE(results[0].address, (uint64_t)4);
QCOMPARE(results[1].address, (uint64_t)20);
}
void scan_constrainRegions_intersectsProviderRegions() {
QByteArray data(256, 0);
data[160] = char(0xCC);
data[210] = char(0xCC);
QVector<MemoryRegion> regions;
regions.append({100, 100, true, false, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\xCC", 1);
req.mask = QByteArray("\xFF", 1);
req.constrainRegions = {{150, 250}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)160);
}
void scan_constrainRegions_noOverlap() {
QByteArray data(32, char(0xEE));
QVector<MemoryRegion> regions;
regions.append({0, 16, true, false, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\xEE", 1);
req.mask = QByteArray("\xFF", 1);
req.constrainRegions = {{100, 200}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 0);
}
// -- constrainRegions edge cases --
void scan_constrainRegions_gapBetweenRegions() {
// Provider has two regions with a gap: [0,16) and [32,48).
// Constraint spans the gap: [8, 40). Should find matches in both.
QByteArray data(64, 0);
data[10] = char(0xDD);
data[35] = char(0xDD);
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, false, {}});
regions.append({32, 16, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xDD));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{8, 40}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
QCOMPARE(results[0].address, (uint64_t)10);
QCOMPARE(results[1].address, (uint64_t)35);
}
void scan_constrainRegions_partialRegionOverlap() {
// Provider region [100, 200). Constraint [150, 250) clips to [150, 200).
QByteArray data(256, 0);
data[120] = char(0xAB);
data[160] = char(0xAB);
QVector<MemoryRegion> regions;
regions.append({100, 100, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xAB));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{150, 250}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)160);
}
void scan_constrainRegions_mixedModuleAndAnonymous() {
// Module region + anonymous heap region. Constraint covers both.
QByteArray data(0x10000, 0);
data[0x1500] = char(0xCC);
data[0x5500] = char(0xCC);
QVector<MemoryRegion> regions;
regions.append({0x1000, 0x1000, true, false, true, QString("game.exe")});
regions.append({0x5000, 0x1000, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xCC));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{0x0, 0x10000}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
QCOMPARE(results[0].address, (uint64_t)0x1500);
QCOMPARE(results[1].address, (uint64_t)0x5500);
}
void scan_constrainRegions_fallbackProvider() {
// BufferProvider returns no regions -> fallback [0, size).
// constrainRegions should still work against the fallback.
QByteArray data(64, 0);
data[10] = char(0xAA);
data[30] = char(0xAA);
data[50] = char(0xAA);
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xAA));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{5, 35}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
QCOMPARE(results[0].address, (uint64_t)10);
QCOMPARE(results[1].address, (uint64_t)30);
}
void scan_constrainRegions_adjacentRegions() {
// Two adjacent regions [0,16) and [16,32). Constraint [8,24) spans both.
QByteArray data(32, 0);
data[12] = char(0xEF);
data[20] = char(0xEF);
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, false, {}});
regions.append({16, 16, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xEF));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{8, 24}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
QCOMPARE(results[0].address, (uint64_t)12);
QCOMPARE(results[1].address, (uint64_t)20);
}
void scan_constrainRegions_writableFilterPreserved() {
// filterWritable=true should still exclude non-writable clipped regions.
QByteArray data(0x4000, 0);
data[0x1100] = char(0xBB);
data[0x2100] = char(0xBB);
QVector<MemoryRegion> regions;
regions.append({0x1000, 0x1000, true, false, true, {}});
regions.append({0x2000, 0x1000, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xBB));
req.mask = QByteArray(1, char(0xFF));
req.filterWritable = true;
req.constrainRegions = {{0x1000, 0x3000}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)0x2100);
}
void scan_constrainRegions_constraintExtendsBeforeAndAfter() {
// Region [10, 20). Constraint [0, 30) extends before and after.
// Should only scan [10, 20) — the intersection.
QByteArray data(32, 0);
data[5] = char(0xAA); // outside region, should NOT be found
data[15] = char(0xAA); // inside region, should be found
data[25] = char(0xAA); // outside region, should NOT be found
QVector<MemoryRegion> regions;
regions.append({10, 10, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xAA));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{0, 30}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)15);
}
void scan_constrainRegions_emptyConstraintScansAll() {
// Empty constrainRegions should scan everything (no restriction).
QByteArray data(32, 0);
data[5] = char(0xBB);
data[15] = char(0xBB);
QVector<MemoryRegion> regions;
regions.append({0, 32, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xBB));
req.mask = QByteArray(1, char(0xFF));
// constrainRegions left empty
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
}
void scan_constrainRegions_singleAddressRange() {
// Equivalent to startAddress/endAddress: single constraint range.
QByteArray data(32, 0);
data[8] = char(0xAA);
data[16] = char(0xAA);
data[24] = char(0xAA);
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xAA));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{8, 20}}; // same as startAddress=8, endAddress=20
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
QCOMPARE(results[0].address, (uint64_t)8);
QCOMPARE(results[1].address, (uint64_t)16);
}
void scan_constrainRegions_withStartEndAddress() {
// Both constrainRegions and startAddress/endAddress set.
// constrainRegions: [0, 16) and [24, 32). startAddress/endAddress: [8, 28).
// Effective scan should be intersection of both: [8, 16) and [24, 28).
// Match at 4 (outside both), 12 (in both), 20 (in startEnd but not constrain),
// 26 (in both), 30 (in constrain but not startEnd).
QByteArray data(32, 0);
data[4] = char(0xDD);
data[12] = char(0xDD);
data[20] = char(0xDD);
data[26] = char(0xDD);
data[30] = char(0xDD);
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xDD));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{0, 16}, {24, 32}};
req.startAddress = 8;
req.endAddress = 28;
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2); // only 12 and 26
QCOMPARE(results[0].address, (uint64_t)12);
QCOMPARE(results[1].address, (uint64_t)26);
}
void scan_constrainRegions_unknownValueScan() {
// Unknown value scan with constrainRegions should only capture within ranges.
QByteArray data(32, char(0x42));
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.condition = ScanCondition::UnknownValue;
req.valueSize = 4;
req.alignment = 4;
req.constrainRegions = {{8, 24}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
// Range [8, 24) = 16 bytes, alignment 4, valueSize 4 -> offsets 8, 12, 16, 20 = 4 results
QCOMPARE(results.size(), 4);
QCOMPARE(results[0].address, (uint64_t)8);
QCOMPARE(results[3].address, (uint64_t)20);
}
void scan_constrainRegions_nonZeroBase() {
// Region with non-zero base; constraint matches exactly.
QByteArray data(0x10000, 0);
data[0x8100] = char(0xFF);
QVector<MemoryRegion> regions;
regions.append({0x8000, 0x1000, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xFF));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{0x8000, 0x9000}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)0x8100);
}
void scan_constrainRegions_zeroSizeConstraint() {
// Degenerate: constraint with start == end (zero size). Should scan nothing.
QByteArray data(32, char(0xAA));
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xAA));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{10, 10}}; // zero-size
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 0);
}
void scan_constrainRegions_invertedRange() {
// Degenerate: constraint with start > end. Should be treated as empty/invalid.
QByteArray data(32, char(0xAA));
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xAA));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{20, 10}}; // inverted
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 0);
}
void scan_constrainRegions_overlappingConstraints() {
// Two overlapping constraints: [4, 20) and [12, 28).
// Should NOT double-count matches in the overlap [12, 20).
QByteArray data(32, 0);
data[8] = char(0xCC);
data[16] = char(0xCC);
data[24] = char(0xCC);
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xCC));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{4, 20}, {12, 28}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
// After merge: [4, 28). All three matches are in range, no duplicates.
QCOMPARE(results.size(), 3);
}
void scan_constrainRegions_patternAtFirstByte() {
// Pattern at the very first byte of a clipped sub-region.
// Region [0, 64). Constraint [20, 40). Match at offset 20.
QByteArray data(64, 0);
data[20] = char(0xFE);
QVector<MemoryRegion> regions;
regions.append({0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0xFE));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{20, 40}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)20);
}
void scan_constrainRegions_patternAtLastByte() {
// Pattern at the very last valid position of a clipped sub-region.
// Region [0, 64). Constraint [20, 40). 4-byte pattern at offset 36 (last valid: 40-4=36).
QByteArray data(64, 0);
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
QVector<MemoryRegion> regions;
regions.append({0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4);
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
req.constrainRegions = {{20, 40}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)36);
}
void scan_constrainRegions_patternOneByteAfterEnd() {
// Pattern starts 1 byte before constraint end — only 3 of 4 bytes are in range.
// Should NOT match because the full pattern doesn't fit.
// Region [0, 64). Constraint [20, 39). 4-byte pattern at offset 36 (needs 36..39, but 39 is excluded).
QByteArray data(64, 0);
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
QVector<MemoryRegion> regions;
regions.append({0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4);
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
req.constrainRegions = {{20, 39}}; // ends at 39, pattern needs 36..39 inclusive
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 0); // pattern doesn't fit
}
void scan_constrainRegions_regionSmallerThanPattern() {
// Clipped sub-region is smaller than the pattern. Should scan nothing, not crash.
// Region [0, 64). Constraint [30, 32). 4-byte pattern can't fit in 2 bytes.
QByteArray data(64, char(0xAA));
QVector<MemoryRegion> regions;
regions.append({0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\xAA\xAA\xAA\xAA", 4);
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
req.constrainRegions = {{30, 32}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 0);
}
void scan_constrainRegions_patternExactlyFitsRegion() {
// Clipped sub-region is exactly pattern size. Should find match if bytes match.
// Region [0, 64). Constraint [30, 34). 4-byte pattern, 4-byte region.
QByteArray data(64, 0);
data[30] = char(0x11); data[31] = char(0x22); data[32] = char(0x33); data[33] = char(0x44);
QVector<MemoryRegion> regions;
regions.append({0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\x11\x22\x33\x44", 4);
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
req.constrainRegions = {{30, 34}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 1);
QCOMPARE(results[0].address, (uint64_t)30);
}
void scan_constrainRegions_matchAtRegionBoundaries() {
// Two adjacent clipped sub-regions. Matches at the last byte of the first
// and first byte of the second. Both should be found.
// Regions: [0, 16) and [16, 32). Constraint [0, 32) (full coverage).
QByteArray data(32, 0);
data[15] = char(0x77); // last byte of first region
data[16] = char(0x77); // first byte of second region
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, false, {}});
regions.append({16, 16, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray(1, char(0x77));
req.mask = QByteArray(1, char(0xFF));
req.constrainRegions = {{0, 32}};
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 2);
QCOMPARE(results[0].address, (uint64_t)15);
QCOMPARE(results[1].address, (uint64_t)16);
}
void scan_constrainRegions_multibyteAtClipBoundary() {
// 4-byte pattern that straddles the constraint boundary — should NOT be found
// because the clipped region doesn't contain the full pattern.
// Region [0, 64). Constraint [10, 13). Pattern at offset 10 is 4 bytes (10..13),
// but constraint end is 13 (exclusive), so only 3 bytes [10,13) are in range.
QByteArray data(64, 0);
data[10] = char(0xAA); data[11] = char(0xBB); data[12] = char(0xCC); data[13] = char(0xDD);
QVector<MemoryRegion> regions;
regions.append({0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = QByteArray("\xAA\xBB\xCC\xDD", 4);
req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4);
req.constrainRegions = {{10, 13}}; // only 3 bytes, pattern needs 4
engine.start(prov, req);
QVERIFY(finSpy.wait(5000));
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QCOMPARE(results.size(), 0);
}
// ── Value type + pattern scans at every position in a constrained region ──
// Helper: run a scan with the given pattern/mask/alignment in a constrained region,
// return the result addresses.
QVector<uint64_t> scanConstrained(const QByteArray& data,
const QByteArray& pat, const QByteArray& mask,
int alignment, uint64_t cStart, uint64_t cEnd) {
auto prov = std::make_shared<BufferProvider>(data);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
ScanRequest req;
req.pattern = pat;
req.mask = mask;
req.alignment = alignment;
req.constrainRegions = {{cStart, cEnd}};
engine.start(prov, req);
if (!finSpy.wait(5000)) return {};
auto results = finSpy.first().first().value<QVector<ScanResult>>();
QVector<uint64_t> addrs;
for (const auto& r : results) addrs.append(r.address);
return addrs;
}
void scan_int32_atRegionStart() {
QByteArray data(128, 0);
int32_t v = 0x12345678;
std::memcpy(data.data() + 32, &v, 4);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Int32, "305419896", pat, mask)); // 0x12345678
auto addrs = scanConstrained(data, pat, mask, 4, 32, 96);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)32);
}
void scan_int32_atRegionEnd() {
QByteArray data(128, 0);
int32_t v = 0x12345678;
// Last aligned 4-byte position in [32, 96) is 92
std::memcpy(data.data() + 92, &v, 4);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Int32, "305419896", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 4, 32, 96);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)92);
}
void scan_float_atRegionStart() {
QByteArray data(128, 0);
float v = 3.14f;
std::memcpy(data.data() + 16, &v, 4);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 4, 16, 80);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)16);
}
void scan_float_atRegionEnd() {
QByteArray data(128, 0);
float v = 3.14f;
// Last aligned 4-byte position in [16, 80) is 76
std::memcpy(data.data() + 76, &v, 4);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 4, 16, 80);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)76);
}
void scan_double_atRegionEnd() {
QByteArray data(128, 0);
double v = 2.71828;
// Last aligned 8-byte position in [0, 128) is 120
std::memcpy(data.data() + 120, &v, 8);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Double, "2.71828", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 8, 0, 128);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)120);
}
void scan_int64_atRegionEnd() {
QByteArray data(128, 0);
int64_t v = 0x0BADC0DEDEADBEEFLL;
// Last aligned 8-byte position in [8, 72) is 64
std::memcpy(data.data() + 64, &v, 8);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Int64, "841540768839352047", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 8, 8, 72);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)64);
}
void scan_utf16_atRegionEnd() {
QByteArray data(128, 0);
// "AB" in UTF-16LE = 4 bytes
uint16_t chars[] = { 'A', 'B' };
// Last aligned 2-byte position where 4 bytes fit in [0, 128) is 124
std::memcpy(data.data() + 124, chars, 4);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::UTF16, "AB", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 2, 0, 128);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)124);
}
void scan_vec3_atRegionEnd() {
QByteArray data(128, 0);
float v[] = { 1.0f, 2.0f, 3.0f }; // 12 bytes
// Last aligned 4-byte position where 12 bytes fit in [0, 128) is 116
std::memcpy(data.data() + 116, v, 12);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Vec3, "1.0 2.0 3.0", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 4, 0, 128);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)116);
}
void scan_pattern_atRegionStart() {
QByteArray data(128, 0);
data[20] = char(0x48); data[21] = char(0x8B); data[22] = char(0x05);
QByteArray pat, mask;
QVERIFY(parseSignature("48 8B 05", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 1, 20, 100);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)20);
}
void scan_pattern_atRegionEnd() {
QByteArray data(128, 0);
// 3-byte pattern, last position in [20, 100) is 97
data[97] = char(0x48); data[98] = char(0x8B); data[99] = char(0x05);
QByteArray pat, mask;
QVERIFY(parseSignature("48 8B 05", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 1, 20, 100);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)97);
}
void scan_pattern_withWildcard_atRegionEnd() {
QByteArray data(128, 0);
// "48 ?? 05" at last position 97 in [20, 100)
data[97] = char(0x48); data[98] = char(0xFF); data[99] = char(0x05);
QByteArray pat, mask;
QVERIFY(parseSignature("48 ?? 05", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 1, 20, 100);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)97);
}
void scan_int32_multiplePositions_inConstrainedRegion() {
// Place int32 at first, middle, and last aligned positions in [32, 96).
// Aligned positions: 32, 36, 40, ..., 88, 92. First=32, last=92, mid=60.
QByteArray data(128, 0);
int32_t v = 0xCAFEBABE;
std::memcpy(data.data() + 32, &v, 4);
std::memcpy(data.data() + 60, &v, 4);
std::memcpy(data.data() + 92, &v, 4);
// Also place one outside the constraint to verify it's excluded
std::memcpy(data.data() + 8, &v, 4);
std::memcpy(data.data() + 100, &v, 4);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::UInt32, "0xCAFEBABE", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 4, 32, 96);
QCOMPARE(addrs.size(), 3);
QCOMPARE(addrs[0], (uint64_t)32);
QCOMPARE(addrs[1], (uint64_t)60);
QCOMPARE(addrs[2], (uint64_t)92);
}
void scan_pattern_multiplePositions_inConstrainedRegion() {
// IDA-style pattern at first, last, and middle of [16, 80).
// Pattern "AA BB" (2 bytes), alignment 1. First=16, last=78, mid=50.
QByteArray data(128, 0);
data[16] = char(0xAA); data[17] = char(0xBB);
data[50] = char(0xAA); data[51] = char(0xBB);
data[78] = char(0xAA); data[79] = char(0xBB);
// Outside constraint
data[10] = char(0xAA); data[11] = char(0xBB);
data[90] = char(0xAA); data[91] = char(0xBB);
QByteArray pat, mask;
QVERIFY(parseSignature("AA BB", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 1, 16, 80);
QCOMPARE(addrs.size(), 3);
QCOMPARE(addrs[0], (uint64_t)16);
QCOMPARE(addrs[1], (uint64_t)50);
QCOMPARE(addrs[2], (uint64_t)78);
}
void scan_int8_alignment1_atRegionEnd() {
// 1-byte value at last byte of constrained region [10, 50).
QByteArray data(64, 0);
data[49] = char(0x7F);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Int8, "127", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 1, 10, 50);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)49);
}
void scan_uint16_alignment2_atRegionEnd() {
// 2-byte value at last aligned-2 position in [10, 50) = offset 48.
QByteArray data(64, 0);
uint16_t v = 0xBEEF;
std::memcpy(data.data() + 48, &v, 2);
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::UInt16, "0xBEEF", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 2, 10, 50);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)48);
}
void scan_alignment4_skipsUnaligned() {
// int32 placed at unaligned offset 18 inside [16, 48). Alignment 4.
// Aligned positions from 16: 16, 20, 24, 28, 32, 36, 40, 44.
// Offset 18 is not aligned to 4 from the region start, so should be skipped.
QByteArray data(64, 0);
int32_t v = 0xDEADBEEF;
std::memcpy(data.data() + 18, &v, 4); // unaligned
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::UInt32, "0xDEADBEEF", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 4, 16, 48);
QCOMPARE(addrs.size(), 0);
}
void scan_alignment8_skipsUnaligned() {
// double placed at offset 12 inside [0, 64). Alignment 8.
// Aligned positions: 0, 8, 16, 24, 32, 40, 48, 56.
// Offset 12 is not 8-aligned, so should be skipped.
QByteArray data(64, 0);
double v = 99.99;
std::memcpy(data.data() + 12, &v, 8); // unaligned
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::Double, "99.99", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 8, 0, 64);
QCOMPARE(addrs.size(), 0);
}
void scan_alignment2_findsAligned_skipsUnaligned() {
// utf16 "Hi" (4 bytes) at aligned offset 20 and unaligned offset 33.
// Constraint [16, 48), alignment 2. Should find only offset 20.
QByteArray data(64, 0);
uint16_t chars[] = { 'H', 'i' };
std::memcpy(data.data() + 20, chars, 4); // aligned to 2
std::memcpy(data.data() + 33, chars, 4); // unaligned to 2
QByteArray pat, mask;
QVERIFY(serializeValue(ValueType::UTF16, "Hi", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 2, 16, 48);
QCOMPARE(addrs.size(), 1);
QCOMPARE(addrs[0], (uint64_t)20);
}
void scan_alignment1_overlappingWrites() {
// Pattern "AA BB" written at 20, then overwritten at 21, plus 25.
// Second write clobbers offset 20's pattern; only 21 and 25 match.
QByteArray data(48, 0);
data[20] = char(0xAA); data[21] = char(0xBB);
data[21] = char(0xAA); data[22] = char(0xBB); // overlapping at 21
data[25] = char(0xAA); data[26] = char(0xBB);
QByteArray pat, mask;
QVERIFY(parseSignature("AA BB", pat, mask));
auto addrs = scanConstrained(data, pat, mask, 1, 16, 32);
QCOMPARE(addrs.size(), 2); // 21 and 25 (20 was overwritten)
QCOMPARE(addrs[0], (uint64_t)21);
QCOMPARE(addrs[1], (uint64_t)25);
}
};
QTEST_MAIN(TestScanner)

View File

@@ -790,6 +790,7 @@ private slots:
QByteArray newBytes(4, '\0');
std::memcpy(newBytes.data(), &newVal, 4);
prov->writeBytes(8, newBytes);
m_panel->valueEdit()->setText("99");
// Click update — runs async
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
@@ -839,6 +840,7 @@ private slots:
std::memcpy(nb.data(), &newVal, 4);
prov->writeBytes(i * 4, nb);
}
m_panel->valueEdit()->setText("21");
// Click Re-scan — runs async
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
@@ -930,6 +932,7 @@ private slots:
QByteArray nb2(4, '\0');
std::memcpy(nb2.data(), &v2, 4);
prov->writeBytes(4, nb2);
m_panel->valueEdit()->setText("20");
{
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
@@ -944,6 +947,7 @@ private slots:
QByteArray nb3(4, '\0');
std::memcpy(nb3.data(), &v3, 4);
prov->writeBytes(4, nb3);
m_panel->valueEdit()->setText("30");
{
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
@@ -1009,6 +1013,7 @@ private slots:
int32_t newVal = kVal + iter;
for (int off = 0; off + 4 <= kBufSize; off += kStride)
std::memcpy(prov->data().data() + off, &newVal, 4);
m_panel->valueEdit()->setText(QString::number(newVal));
QElapsedTimer iterTimer;
iterTimer.start();