Compare commits

...

23 Commits

Author SHA1 Message Date
IChooseYou
cb10bc8a82 docs: remove dedicated kernel driver section from README 2026-03-14 17:47:07 -06:00
IChooseYou
b5521bd638 docs: add kernel driver plugin to README
Document the KernelMemory plugin — capabilities, driver build
instructions, and architecture diagram.
2026-03-14 17:45:21 -06:00
IChooseYou
89d6e1944b fix: guard computeOffset against negative results before address arithmetic
computeOffset() returns int64_t but most callers added the result directly
to baseAddress (uint64_t) without checking for negative values. A malformed
tree with negative cumulative offsets would produce wrapped addresses,
potentially reading/writing arbitrary memory in the bitfield toggle and
edit paths. Added sign checks at all 9 unguarded call sites.
2026-03-14 17:31:13 -06:00
IChooseYou
7528d1bbbb Merge pull request #12 from 70RMUND/fix/linux-menubar-toolbuttons
fix: Linux menu bar horizontal layout via QToolButton fallback
2026-03-14 16:07:39 -06:00
Your Name
4f2288048e fix: Linux menu bar renders as horizontal tool buttons instead of collapsed extension popup
On Linux, QMenuBar inside a custom title bar widget (setMenuWidget) collapses
all items into the extension overflow popup. Replace with QToolButton widgets
on Linux that share the same QMenu objects. Includes hover-to-switch behavior
via event filter on open menus.

Windows and macOS paths are unchanged — guarded by #ifdef __linux__ and
runtime m_useToolButtons flag.
2026-03-14 15:51:31 -04:00
IChooseYou
97b6f55e1f fix: Linux menu popups and SVG icon rendering
- Guard FramelessWindowHint + WA_TranslucentBackground on QMenu to
  Windows only — breaks popup submenus on Linux/Wayland compositors
- Render SVG icons via QSvgRenderer to QPixmap explicitly, avoiding
  dependency on the qsvgicon image format plugin which may not be
  deployed on Linux
2026-03-14 12:41:11 -06:00
IChooseYou
6a30e0a402 fix: replace remaining QList::append({}) in plugins and tests
Missed plugin and test directories in the previous Qt 6.8 compat fix.
2026-03-14 12:11:08 -06:00
IChooseYou
1501a1542c feat: symbol double-click navigation, tree icons, and module.dll address parsing
- Double-click symbol in Symbols tab navigates to moduleBase + RVA
- Add symbol-method.svg icon for function symbols, symbol-structure.svg for modules
- Force-populate all modules on search so filter works without expanding first
- Parse module.dll/exe/sys as identifiers in address bar (e.g. client.dll + 0xFF)
2026-03-14 11:57:32 -06:00
IChooseYou
4f82b39785 fix: uint64_t to QVariant ambiguity on Qt 6.8 Linux 2026-03-14 11:52:45 -06:00
IChooseYou
009ddc951c fix: commit remaining uncommitted source changes for CI
Add extractPdbSymbols declaration to import_pdb.h,
enumerateModules to provider.h, and other pending changes
that were only local.
2026-03-14 09:36:49 -06:00
IChooseYou
5921af2b4f fix: replace QList::append({}) with push_back/emplaceBack for Qt 6.8
Qt 6.8's stricter QList rejects brace-enclosed initializer lists in
append(). Fixed 43 call sites across 13 files.
2026-03-14 09:21:14 -06:00
IChooseYou
5ded192990 fix: sync tab title on keyword convert, add new screenshots
- Update dock tab title when converting enum/class via workspace menu
- Add tooltip and source picker screenshots to README
2026-03-14 09:14:28 -06:00
IChooseYou
54bee5022b fix: add missing symbols dock declarations to mainwindow.h 2026-03-14 09:03:41 -06:00
IChooseYou
5d2d324946 fix: add missing symbol store and PDB debug info sources
These files were referenced in CMakeLists.txt and main.cpp but
never committed, breaking the CI build.
2026-03-14 08:13:58 -06:00
IChooseYou
5b2cf1ae1f feat: arrow tooltip improvements and base address cheat sheet
- Scale tooltip font to 90% of editor font
- Replace inline edit hint for base address with hover tooltip
- Two-column cheat sheet: syntax examples + explanations
- Dismiss all popups on alt-tab (ActivationChange)
2026-03-14 08:03:23 -06:00
IChooseYou
f1a36f2ad3 feat: custom arrow tooltip with transparent background
Rewrite RcxTooltip to use WA_TranslucentBackground with a single
contiguous QPainterPath (rounded rect + arrow notch). Pre-set the
DarkTitleBar property to prevent DarkApp from calling
DwmSetWindowAttribute which breaks layered window compositing.

Dismiss all popups (including arrow tooltip) on alt-tab via
MainWindow::changeEvent(ActivationChange).
2026-03-14 06:45:45 -06:00
IChooseYou
665138e688 fix: force all nodes collapsed on file load
Prevents 512-element arrays from expanding on load and triggering
thousands of memory reads. Root nodes still show children via
isRootHeader override.
2026-03-14 06:05:53 -06:00
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
63 changed files with 6634 additions and 1289 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

@@ -147,6 +147,12 @@ add_executable(Reclass
src/mcp/mcp_bridge.cpp
src/addressparser.h
src/addressparser.cpp
src/symbolstore.h
src/symbolstore.cpp
src/symbol_downloader.h
src/symbol_downloader.cpp
src/imports/pe_debug_info.h
src/imports/pe_debug_info.cpp
src/disasm.h
src/disasm.cpp
third_party/fadec/decode.c
@@ -415,7 +421,7 @@ if(BUILD_TESTING)
if(BUILD_UI_TESTS)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -429,7 +435,7 @@ if(BUILD_TESTING)
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -443,7 +449,7 @@ if(BUILD_TESTING)
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_source_management tests/test_source_management.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -475,7 +481,7 @@ if(BUILD_TESTING)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -489,7 +495,7 @@ if(BUILD_TESTING)
add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_type_visibility tests/test_type_visibility.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -509,7 +515,7 @@ if(BUILD_TESTING)
add_test(NAME test_options_dialog COMMAND test_options_dialog)
add_executable(test_source_provider tests/test_source_provider.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
@@ -544,6 +550,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
@@ -587,6 +604,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

@@ -22,6 +22,10 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor
## Screenshots
![Base address tooltip with expression cheat sheet](docs/README_PIC5.png)
![Data source picker with saved sources](docs/README_PIC4.png)
![Windows — VTable with value history popup](docs/README_PIC1.png)
![macOS — project tree with kernel struct inspection](docs/README_PIC2.png)
@@ -77,6 +81,7 @@ Full command stack with 15 undoable operations: ChangeKind, Rename, Collapse, In
- **File** — open any binary file and inspect its contents as structured data
- **Process** — attach to a live process and read its memory in real time (Windows/Linux)
- **Kernel driver** — Windows kernel driver (IOCTL) for process memory, physical memory, page table walking, and CR3/VTOP translation
- **Remote Process** — read another process's memory over TCP with cross-architecture 32/64-bit support
- **WinDbg** — connect to live WinDbg debugging sessions or load crash dumps
- **Saved sources** — quick-switch between recently used data sources per tab
@@ -90,6 +95,7 @@ DLL plugins loaded from a `Plugins` folder, auto or manual.
| Plugin | Description |
|--------|-------------|
| **Process memory** | Attach to local processes on Windows and Linux — PID-based, with symbol resolution and module/region enumeration |
| **Kernel memory** | Windows kernel driver (IOCTL) for reading/writing process and physical memory, CR3 queries, virtual-to-physical translation, and full 4-level page table walking — supports 4KB, 2MB, and 1GB pages |
| **WinDbg** | Access data from live WinDbg debugging sessions |
| **Remote process memory** | TCP RPC-based remote process access with cross-architecture support |
| **ReClass.NET compatibility** | Load existing ReClass.NET native DLL plugins directly; optional .NET CLR hosting for managed plugins |

BIN
docs/README_PIC4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/README_PIC5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

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.push_back(ThreadInfo{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.push_back(ModuleInfo{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

@@ -185,8 +185,14 @@ void ProcessMemoryProvider::cacheModules()
if ( i == 0 )
m_base = (uint64_t)mi.lpBaseOfDll;
m_modules.append({
WCHAR modPath[MAX_PATH];
QString fullPath;
if (GetModuleFileNameExW(m_handle, mods[i], modPath, MAX_PATH))
fullPath = QString::fromWCharArray(modPath);
m_modules.push_back(ModuleInfo{
QString::fromWCharArray(modName),
fullPath,
(uint64_t)mi.lpBaseOfDll,
(uint64_t)mi.SizeOfImage
});
@@ -194,6 +200,15 @@ void ProcessMemoryProvider::cacheModules()
}
}
QVector<rcx::Provider::ModuleEntry> ProcessMemoryProvider::enumerateModules() const
{
QVector<ModuleEntry> result;
result.reserve(m_modules.size());
for (const auto& m : m_modules)
result.push_back(ModuleEntry{m.name, m.fullPath, m.base, m.size});
return result;
}
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
@@ -400,8 +415,9 @@ void ProcessMemoryProvider::cacheModules()
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
{
QFileInfo fi(it.key());
m_modules.append({
m_modules.push_back(ModuleInfo{
fi.fileName(),
it.key(),
it->base,
it->end - it->base
});
@@ -530,7 +546,7 @@ QVector<rcx::Provider::ThreadInfo> ProcessMemoryProvider::tebs() const
ULONG tbiLen = 0;
NTSTATUS qitSt = pNtQIT(hThread, 0, &tbi, sizeof(tbi), &tbiLen);
if (qitSt >= 0 && tbi.TebBaseAddress)
result.append({(uint64_t)(uintptr_t)tbi.TebBaseAddress, tid});
result.push_back(ThreadInfo{(uint64_t)(uintptr_t)tbi.TebBaseAddress, tid});
CloseHandle(hThread);
}
break;

View File

@@ -43,6 +43,7 @@ public:
void refreshModules() { m_modules.clear(); cacheModules(); }
uint64_t peb() const override { return m_peb; }
QVector<ThreadInfo> tebs() const override;
QVector<ModuleEntry> enumerateModules() const override;
private:
void cacheModules();
@@ -62,6 +63,7 @@ private:
struct ModuleInfo {
QString name;
QString fullPath;
uint64_t base;
uint64_t size;
};

View File

@@ -244,7 +244,7 @@ struct IpcClient {
reinterpret_cast<const char*>(data + entry->nameOffset),
(int)entry->nameLength);
#endif
result.append({modName, entry->base, entry->size});
result.push_back(RemoteProcessProvider::ModuleInfo{modName, entry->base, entry->size});
}
return result;
}

View File

@@ -31,6 +31,7 @@ namespace rcx {
//
// All numeric literals are hexadecimal (base 16).
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
// Module names with extensions (e.g. "client.dll") are scanned as one token.
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
class ExpressionParser {
@@ -273,16 +274,46 @@ 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.
// WinDbg-style "module!symbol" is scanned as a single identifier token.
// 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;
// Scan full token
// Scan full token, including "module!symbol" as one token
while (!atEnd() && isIdentChar(peek())) {
if (!isHexDigit(peek()))
hasNonHex = true;
advance();
}
// Handle module.dll / module.exe / module.sys extensions
// e.g. "client.dll + 0xFF" should parse "client.dll" as one token
if (!atEnd() && peek() == '.' && m_pos > start) {
int dotPos = m_pos;
advance(); // skip '.'
int extStart = m_pos;
while (!atEnd() && isIdentChar(peek()))
advance();
if (m_pos > extStart) {
hasNonHex = true; // '.' makes it definitively an identifier
} else {
m_pos = dotPos; // backtrack — '.' at end isn't an extension
}
}
// If we hit '!' and the next char is an identifier start, extend the token
// to include the second part (WinDbg module!symbol syntax)
if (!atEnd() && peek() == '!' && m_pos > start) {
int bangPos = m_pos;
advance(); // skip '!'
if (!atEnd() && isIdentStart(peek())) {
hasNonHex = true;
while (!atEnd() && isIdentChar(peek())) {
advance();
}
} else {
m_pos = bangPos; // backtrack — '!' at end isn't module!symbol
}
}
QString token = m_input.mid(start, m_pos - start);
@@ -292,6 +323,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 +341,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

@@ -71,6 +71,7 @@ struct ComposeState {
bool treeLines = false; // draw Unicode tree connectors in indentation
bool braceWrap = false; // opening brace on its own line
bool typeHints = false; // show type inference hints on hex nodes
SymbolLookupFn symbolLookup; // optional PDB symbol lookup callback
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
@@ -262,7 +263,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
auto suggestions = inferTypes(
reinterpret_cast<const uint8_t*>(b.constData()), sz);
if (!suggestions.isEmpty() && suggestions[0].strength >= 3) {
lm.typeHintStart = lineText.size() + 2; // after " " gap
lm.typeHintStart = kFoldCol + lineText.size() + 2; // after fold prefix + " " gap
lm.typeHintKinds = suggestions[0].kinds;
QString typeName = formatHint(suggestions[0]);
QString preview = formatPreview(
@@ -276,6 +277,13 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
}
}
// PDB symbol annotation: show symbol name if this address matches a loaded symbol
if (sub == 0 && state.symbolLookup) {
QString sym = state.symbolLookup(absAddr);
if (!sym.isEmpty())
lineText += QStringLiteral(" // ") + sym;
}
state.emitLine(lineText, std::move(lm));
}
}
@@ -695,6 +703,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 +840,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 +943,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 +1013,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);
@@ -1040,12 +1095,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
bool compactColumns, bool treeLines, bool braceWrap,
bool typeHints) {
bool typeHints, SymbolLookupFn symbolLookup) {
ComposeState state;
state.compactColumns = compactColumns;
state.treeLines = treeLines;
state.braceWrap = braceWrap;
state.typeHints = typeHints;
state.symbolLookup = std::move(symbolLookup);
// Precompute parent→children map
for (int i = 0; i < tree.nodes.size(); i++)

View File

@@ -1,5 +1,6 @@
#include "controller.h"
#include "addressparser.h"
#include "symbolstore.h"
#include "typeselectorpopup.h"
#include "providerregistry.h"
#include "themes/thememanager.h"
@@ -17,6 +18,7 @@
#include <QFileDialog>
#include <QMessageBox>
#include <QSettings>
#include <QRegularExpression>
#include <QtConcurrent/QtConcurrentRun>
#include <limits>
@@ -73,8 +75,10 @@ RcxDocument::RcxDocument(QObject* parent)
}
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
bool treeLines, bool braceWrap, bool typeHints) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap, typeHints);
bool treeLines, bool braceWrap, bool typeHints,
SymbolLookupFn symbolLookup) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap, typeHints,
std::move(symbolLookup));
}
bool RcxDocument::save(const QString& path) {
@@ -268,6 +272,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
// Footer "Trim" button — remove trailing hex nodes from end of struct
connect(editor, &RcxEditor::trimHexRequested,
this, [this](uint64_t structId) {
// Unions don't have trailing padding — all members overlap at offset 0
int si = m_doc->tree.indexOfId(structId);
if (si >= 0 && m_doc->tree.nodes[si].classKeyword == QStringLiteral("union"))
return;
QVector<int> children = m_doc->tree.childrenOf(structId);
if (children.isEmpty()) return;
@@ -303,7 +311,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
auto oldMembers = members;
for (int i = 0; i < count; i++)
members.append({QStringLiteral("Member%1").arg(nextVal + i), nextVal + i});
members.emplaceBack(QStringLiteral("Member%1").arg(nextVal + i), nextVal + i);
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{enumId, oldMembers, members}));
});
@@ -441,13 +449,38 @@ void RcxController::connectEditor(RcxEditor* editor) {
*ok = prov->read(addr, &val, ptrSz);
return val;
};
cbs.resolveIdentifier = [prov](const QString& name, bool* ok) -> uint64_t {
return SymbolStore::instance().resolve(name, prov, ok);
};
// 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/symbol syntax
static const QRegularExpression formulaRx(
QStringLiteral("[<\\[]|\\b(?:vtop|cr3|phys)\\s*\\(|\\w+!\\w+"));
QString newFormula = formulaRx.match(s).hasMatch() ? s : QString();
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula}));
}
@@ -608,11 +641,20 @@ void RcxController::refresh() {
// Bracket compose with thread-local doc pointer for type name resolution
s_composeDoc = m_doc;
// Build symbol lookup callback if PDB symbols are loaded
SymbolLookupFn symLookup;
if (SymbolStore::instance().hasSymbols() && m_doc->provider) {
auto* prov = m_doc->provider.get();
symLookup = [prov](uint64_t addr) -> QString {
return SymbolStore::instance().getSymbolForAddress(addr, prov);
};
}
// Compose against snapshot provider if active, otherwise real provider
if (m_snapshotProv)
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints);
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints, symLookup);
else
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints);
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints, symLookup);
s_composeDoc = nullptr;
@@ -621,6 +663,7 @@ void RcxController::refresh() {
for (auto& lm : m_lastResult.meta) {
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
int64_t offset = m_doc->tree.computeOffset(lm.nodeIdx);
if (offset < 0) continue;
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
if (isHexPreview(node.kind)) {
@@ -813,7 +856,7 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
if (si == nodeIdx) continue;
auto& sib = m_doc->tree.nodes[si];
if (sib.offset >= oldEnd)
adjs.append({sib.id, sib.offset, sib.offset + delta});
adjs.push_back(cmd::OffsetAdj{sib.id, sib.offset, sib.offset + delta});
}
}
bool needsRename = isHexNode(node.kind) && !isHexNode(newKind);
@@ -887,7 +930,7 @@ void RcxController::insertNodeAbove(int beforeIdx, NodeKind kind, const QString&
for (int si : siblings) {
auto& sib = m_doc->tree.nodes[si];
if (sib.offset >= before.offset)
adjs.append({sib.id, sib.offset, sib.offset + insertSize});
adjs.push_back(cmd::OffsetAdj{sib.id, sib.offset, sib.offset + insertSize});
}
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
@@ -912,7 +955,7 @@ void RcxController::removeNode(int nodeIdx) {
if (si == nodeIdx) continue;
auto& sib = m_doc->tree.nodes[si];
if (sib.offset >= deletedEnd) {
adjs.append({sib.id, sib.offset, sib.offset - deletedSize});
adjs.push_back(cmd::OffsetAdj{sib.id, sib.offset, sib.offset - deletedSize});
}
}
}
@@ -1419,7 +1462,7 @@ void RcxController::duplicateNode(int nodeIdx) {
if (si == nodeIdx) continue;
auto& sib = m_doc->tree.nodes[si];
if (sib.offset >= copyOffset)
adjs.append({sib.id, sib.offset, sib.offset + copySize});
adjs.push_back(cmd::OffsetAdj{sib.id, sib.offset, sib.offset + copySize});
}
}
@@ -1565,7 +1608,9 @@ void RcxController::toggleBitfieldBit(uint64_t nodeId, int memberIdx) {
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
const auto& bm = node.bitfieldMembers[memberIdx];
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
int64_t signedOff = m_doc->tree.computeOffset(ni);
if (signedOff < 0) return;
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedOff);
int containerSize = sizeForKind(node.elementKind);
if (containerSize <= 0) containerSize = 4;
@@ -1593,7 +1638,9 @@ void RcxController::editBitfieldValue(uint64_t nodeId, int memberIdx) {
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
const auto& bm = node.bitfieldMembers[memberIdx];
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
int64_t signedOff = m_doc->tree.computeOffset(ni);
if (signedOff < 0) return;
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedOff);
int containerSize = sizeForKind(node.elementKind);
if (containerSize <= 0) containerSize = 4;
@@ -1739,14 +1786,24 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// ── Insert ► submenu ──
{
// Find earliest selected node (lowest offset) for insert-above
int firstIdx = -1;
int lowestOff = INT_MAX;
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0 && m_doc->tree.nodes[idx].offset < lowestOff) {
lowestOff = m_doc->tree.nodes[idx].offset;
firstIdx = idx;
}
}
auto* insertMenu = menu.addMenu(icon("diff-added.svg"), "Insert");
insertMenu->addAction("Insert 4", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
insertMenu->addAction("Insert 4 Above", [this, firstIdx]() {
if (firstIdx >= 0)
insertNodeAbove(firstIdx, NodeKind::Hex32, QStringLiteral("field"));
});
insertMenu->addAction("Insert 8", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
insertMenu->addAction("Insert 8 Above", [this, firstIdx]() {
if (firstIdx >= 0)
insertNodeAbove(firstIdx, NodeKind::Hex64, QStringLiteral("field"));
});
}
@@ -1812,7 +1869,9 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
for (uint64_t id : ids) {
int ni = m_doc->tree.indexOfId(id);
if (ni < 0) continue;
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
int64_t off = m_doc->tree.computeOffset(ni);
if (off < 0) continue;
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(off);
addrs << QStringLiteral("0x") + QString::number(addr, 16).toUpper();
}
QApplication::clipboard()->setText(addrs.join('\n'));
@@ -1896,7 +1955,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
auto members = m_doc->tree.nodes[ni].enumMembers;
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
auto oldMembers = members;
members.append({QStringLiteral("NewMember"), nextVal});
members.emplaceBack(QStringLiteral("NewMember"), nextVal);
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
});
@@ -2406,7 +2465,9 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, copyNodeId]() {
int ni = m_doc->tree.indexOfId(copyNodeId);
if (ni < 0) return;
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
int64_t off = m_doc->tree.computeOffset(ni);
if (off < 0) return;
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(off);
QApplication::clipboard()->setText(
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
});
@@ -2440,6 +2501,106 @@ 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) {
int64_t nodeOff = m_doc->tree.computeOffset(nodeIdx);
uint64_t nodeAddr = (nodeOff >= 0)
? m_doc->tree.baseAddress + static_cast<uint64_t>(nodeOff) : 0;
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;
int64_t nodeOff = m_doc->tree.computeOffset(nodeIdx);
if (nodeOff < 0) break;
uint64_t nodeAddr = m_doc->tree.baseAddress
+ static_cast<uint64_t>(nodeOff);
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);
}
@@ -3208,6 +3369,29 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
*ok = prov->read(addr, &val, ptrSz);
return val;
};
cbs.resolveIdentifier = [prov](const QString& name, bool* ok) -> uint64_t {
return SymbolStore::instance().resolve(name, prov, ok);
};
// 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;
@@ -3330,6 +3514,29 @@ void RcxController::selectSource(const QString& text) {
*ok = prov->read(addr, &val, ptrSz);
return val;
};
cbs.resolveIdentifier = [prov](const QString& name, bool* ok) -> uint64_t {
return SymbolStore::instance().resolve(name, prov, ok);
};
// 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)
@@ -3456,7 +3663,7 @@ void RcxController::collectPointerRanges(
int span = m_doc->tree.structSpan(structId);
if (span <= 0) return;
ranges.append({memBase, span});
ranges.emplaceBack(memBase, span);
if (!m_snapshotProv) return;
@@ -3504,7 +3711,7 @@ void RcxController::onRefreshTick() {
// Collect all needed ranges: main struct + pointer targets (absolute addresses)
QVector<QPair<uint64_t,int>> ranges;
ranges.append({m_doc->tree.baseAddress, extent});
ranges.emplaceBack(m_doc->tree.baseAddress, extent);
if (m_snapshotProv) {
QSet<QPair<uint64_t,uint64_t>> visited;
@@ -3607,6 +3814,7 @@ int RcxController::computeDataExtent() const {
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
const Node& node = m_doc->tree.nodes[i];
int64_t off = m_doc->tree.computeOffset(i);
if (off < 0) continue;
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
? m_doc->tree.structSpan(node.id) : node.byteSize();
int64_t end = off + sz;

View File

@@ -42,7 +42,8 @@ public:
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
bool treeLines = false, bool braceWrap = false,
bool typeHints = false) const;
bool typeHints = false,
SymbolLookupFn symbolLookup = {}) const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
@@ -163,6 +164,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,9 +286,10 @@ 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);
n.collapsed = true; // Always load collapsed; user expands as needed
n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
@@ -293,8 +297,8 @@ struct Node {
QJsonArray arr = o["enumMembers"].toArray();
for (const auto& v : arr) {
QJsonObject em = v.toObject();
n.enumMembers.append({em["name"].toString(),
em["value"].toString("0").toLongLong()});
n.enumMembers.emplaceBack(em["name"].toString(),
em["value"].toString("0").toLongLong());
}
}
if (o.contains("bitfieldMembers")) {
@@ -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) ──
@@ -1038,8 +1043,13 @@ namespace fmt {
// ── Compose function forward declaration ──
// Optional callback: given an absolute address, return a symbol name (e.g. "nt!PsActiveProcessHead")
// or empty string if no symbol matches. Used for PDB symbol annotations on rows.
using SymbolLookupFn = std::function<QString(uint64_t addr)>;
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false, bool treeLines = false,
bool braceWrap = false, bool typeHints = false);
bool braceWrap = false, bool typeHints = false,
SymbolLookupFn symbolLookup = {});
} // namespace rcx

View File

@@ -1,6 +1,7 @@
#include "editor.h"
#include "disasm.h"
#include "providerregistry.h"
#include "rcxtooltip.h"
#include <QDebug>
#include <Qsci/qsciscintilla.h>
#include <Qsci/qsciscintillabase.h>
@@ -1397,6 +1398,7 @@ void RcxEditor::dismissAllPopups() {
if (m_historyPopup) static_cast<HoverPopup*>(m_historyPopup)->dismiss();
if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
if (m_arrowTooltip) static_cast<RcxTooltip*>(m_arrowTooltip)->dismiss();
}
void RcxEditor::hideFindBar() {
@@ -2377,12 +2379,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;
@@ -2410,17 +2406,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;
@@ -2865,21 +2853,21 @@ 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)
@@ -2893,8 +2881,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
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"));
} else if (target == EditTarget::BaseAddress) {
// No inline hint — the hover tooltip already shows examples
}
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
// and exit early above (never reach here).
@@ -3062,26 +3051,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,
@@ -3095,11 +3066,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();
@@ -3794,6 +3767,82 @@ void RcxEditor::applyHoverCursor() {
// else: desired stays Arrow (hovering over column padding)
}
// ── Arrow tooltip on command row spans ──
{
bool showTip = false;
if (tokenHit && h.line == 0 && h.line < m_meta.size()
&& m_meta[0].lineKind == LineKind::CommandRow) {
NormalizedSpan span;
QString lineText;
if (resolvedSpanFor(0, t, span, &lineText)
&& h.col >= span.start && h.col < span.end) {
QString tipTitle, tipBody;
switch (t) {
case EditTarget::Source:
tipTitle = QStringLiteral("Data Source");
tipBody = QStringLiteral("Click to change the attached\nmemory source (process, file)");
break;
case EditTarget::BaseAddress:
tipTitle = QStringLiteral("Base Address");
tipBody = QStringLiteral(
"0x7FF61234ABCD hex address\n"
"<app.exe> module base\n"
"<app.exe> + 0x1A0 module + offset\n"
"[<app.exe> + 0x58] follow pointer\n"
"ntdll!SymbolName PDB symbol\n"
"\n"
"Operators: + - * << >> & | ^\n"
"All numbers are hexadecimal");
break;
case EditTarget::RootClassName:
tipTitle = QStringLiteral("Class Name");
tipBody = QStringLiteral("Click to rename this type");
break;
case EditTarget::TypeSelector:
tipTitle = QStringLiteral("Switch View");
tipBody = QStringLiteral("View a different struct in this tab");
break;
default: break;
}
if (!tipTitle.isEmpty()) {
if (!m_arrowTooltip) {
m_arrowTooltip = new RcxTooltip(this);
static_cast<RcxTooltip*>(m_arrowTooltip)->onMouseMove =
[this](QMouseEvent* e) {
QPoint gp = e->globalPosition().toPoint();
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
m_lastHoverPos = vp;
m_hoverInside = m_sci->viewport()->rect().contains(vp);
applyHoverCursor();
};
}
auto* tip = static_cast<RcxTooltip*>(m_arrowTooltip);
const auto& theme = ThemeManager::instance().current();
tip->setTheme(theme.backgroundAlt, theme.border,
theme.text, theme.textDim, theme.border);
tip->populate(tipTitle, tipBody, editorFont());
// Anchor at center of the hovered span, bottom edge of line
long posA = posFromCol(m_sci, 0, span.start);
long posB = posFromCol(m_sci, 0, span.end);
int xA = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_POINTXFROMPOSITION, 0UL, posA);
int xB = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_POINTXFROMPOSITION, 0UL, posB);
int py = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_POINTYFROMPOSITION, 0UL, posA);
int lh = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_TEXTHEIGHT, 0UL);
QPoint anchor = m_sci->viewport()->mapToGlobal(
QPoint((xA + xB) / 2, py + lh));
tip->showAt(anchor);
showTip = true;
}
}
}
if (!showTip && m_arrowTooltip && m_arrowTooltip->isVisible())
static_cast<RcxTooltip*>(m_arrowTooltip)->dismiss();
}
m_sci->viewport()->setCursor(desired);
}
@@ -3860,12 +3909,8 @@ 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) {
if (m_editState.target == EditTarget::BaseAddress)
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
else
setEditComment("Enter=Save Esc=Cancel");
}
if (stateChanged)
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:
@@ -163,6 +159,7 @@ private:
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
QWidget* m_disasmPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
QWidget* m_structPreviewPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
QWidget* m_arrowTooltip = nullptr; // RcxTooltip (arrow callout)
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr;

126
src/examples/PageTables.rcx Normal file
View File

@@ -0,0 +1,126 @@
{
"baseAddress": "0",
"nextId": "2000",
"nodes": [
{
"id": "100",
"kind": "Struct",
"name": "pte",
"structTypeName": "X64_PTE",
"classKeyword": "bitfield",
"elementKind": "UInt64",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true,
"arrayLen": 1,
"strLen": 64,
"bitfieldMembers": [
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
{"name": "PhysAddr", "bitOffset": 12, "bitWidth": 40},
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
]
},
{
"id": "200",
"kind": "Struct",
"name": "page_table",
"structTypeName": "X64_PAGE_TABLE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "201",
"kind": "Array",
"name": "entries",
"offset": 0,
"parentId": "200",
"refId": "100",
"elementKind": "Struct",
"arrayLen": 512,
"strLen": 64,
"collapsed": true
},
{
"id": "300",
"kind": "Struct",
"name": "pde_2mb",
"structTypeName": "X64_PDE_LARGE",
"classKeyword": "bitfield",
"elementKind": "UInt64",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true,
"arrayLen": 1,
"strLen": 64,
"bitfieldMembers": [
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
{"name": "PAT", "bitOffset": 12, "bitWidth": 1},
{"name": "Reserved", "bitOffset": 13, "bitWidth": 8},
{"name": "PhysAddr", "bitOffset": 21, "bitWidth": 31},
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
]
},
{
"id": "400",
"kind": "Struct",
"name": "pdpte_1gb",
"structTypeName": "X64_PDPTE_HUGE",
"classKeyword": "bitfield",
"elementKind": "UInt64",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true,
"arrayLen": 1,
"strLen": 64,
"bitfieldMembers": [
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
{"name": "PAT", "bitOffset": 12, "bitWidth": 1},
{"name": "Reserved", "bitOffset": 13, "bitWidth": 17},
{"name": "PhysAddr", "bitOffset": 30, "bitWidth": 22},
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
]
}
],
"rootIds": ["200"]
}

View File

@@ -396,7 +396,7 @@ uint64_t PdbCtx::importEnum(uint32_t typeIndex) {
field->data.LF_ENUMERATE.value,
field->data.LF_ENUMERATE.lfEasy.kind);
if (eName)
s.enumMembers.append({QString::fromUtf8(eName), val});
s.enumMembers.emplaceBack(QString::fromUtf8(eName), val);
i += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
i += strnlen(eName, maxSize - i - 1) + 1;
@@ -880,7 +880,7 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.bitfieldMembers.append({name, bitPos, bitLen});
n.bitfieldMembers.push_back(BitfieldMember{name, bitPos, bitLen});
tree.addNode(n);
break;
}
@@ -943,6 +943,118 @@ struct PdbFile {
}
};
// ── Public API: extractPdbSymbols ──
PdbSymbolResult extractPdbSymbols(const QString& pdbPath, QString* errorMsg) {
auto setErr = [&](const QString& msg) { if (errorMsg) *errorMsg = msg; };
MappedFile mapped;
if (!QFile::exists(pdbPath)) {
setErr(QStringLiteral("PDB file not found: ") + pdbPath);
return {};
}
if (!mapped.open(pdbPath)) {
setErr(QStringLiteral("Failed to memory-map PDB file: ") + pdbPath);
return {};
}
if (PDB::ValidateFile(mapped.base, mapped.size) != PDB::ErrorCode::Success) {
setErr(QStringLiteral("Invalid PDB file: ") + pdbPath);
return {};
}
PDB::RawFile rawFile = PDB::CreateRawFile(mapped.base);
if (PDB::HasValidDBIStream(rawFile) != PDB::ErrorCode::Success) {
setErr(QStringLiteral("PDB has no valid DBI stream: ") + pdbPath);
return {};
}
const PDB::DBIStream dbiStream = PDB::CreateDBIStream(rawFile);
// Validate required sub-streams
if (dbiStream.HasValidSymbolRecordStream(rawFile) != PDB::ErrorCode::Success ||
dbiStream.HasValidPublicSymbolStream(rawFile) != PDB::ErrorCode::Success ||
dbiStream.HasValidImageSectionStream(rawFile) != PDB::ErrorCode::Success) {
setErr(QStringLiteral("PDB DBI stream missing required sub-streams"));
return {};
}
const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawFile);
const PDB::CoalescedMSFStream symbolRecordStream = dbiStream.CreateSymbolRecordStream(rawFile);
PdbSymbolResult result;
// Derive module name from PDB filename (e.g. "ntoskrnl.pdb" → "ntoskrnl")
QFileInfo fi(pdbPath);
result.moduleName = fi.completeBaseName();
// Read public symbols (S_PUB32)
const PDB::PublicSymbolStream publicSymbolStream = dbiStream.CreatePublicSymbolStream(rawFile);
{
const PDB::ArrayView<PDB::HashRecord> hashRecords = publicSymbolStream.GetRecords();
const size_t count = hashRecords.GetLength();
result.symbols.reserve(static_cast<int>(count));
for (const PDB::HashRecord& hashRecord : hashRecords) {
const PDB::CodeView::DBI::Record* record =
publicSymbolStream.GetRecord(symbolRecordStream, hashRecord);
if (record->header.kind != PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
continue;
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_PUB32.section, record->data.S_PUB32.offset);
if (rva == 0u)
continue;
result.symbols.push_back(PdbSymbol{QString::fromUtf8(record->data.S_PUB32.name), rva});
}
}
// Read global symbols (S_GDATA32, S_GTHREAD32, S_LDATA32, S_LTHREAD32, S_GPROC32, S_LPROC32)
if (dbiStream.HasValidGlobalSymbolStream(rawFile) == PDB::ErrorCode::Success) {
const PDB::GlobalSymbolStream globalSymbolStream = dbiStream.CreateGlobalSymbolStream(rawFile);
const PDB::ArrayView<PDB::HashRecord> hashRecords = globalSymbolStream.GetRecords();
result.symbols.reserve(result.symbols.size() + static_cast<int>(hashRecords.GetLength()));
for (const PDB::HashRecord& hashRecord : hashRecords) {
const PDB::CodeView::DBI::Record* record =
globalSymbolStream.GetRecord(symbolRecordStream, hashRecord);
const char* name = nullptr;
uint32_t rva = 0u;
if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32) {
name = record->data.S_GDATA32.name;
rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_GDATA32.section, record->data.S_GDATA32.offset);
} else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32) {
name = record->data.S_GTHREAD32.name;
rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_GTHREAD32.section, record->data.S_GTHREAD32.offset);
} else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32) {
name = record->data.S_LDATA32.name;
rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_LDATA32.section, record->data.S_LDATA32.offset);
} else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32) {
name = record->data.S_LTHREAD32.name;
rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset);
}
if (rva == 0u)
continue;
if (!name || name[0] == '\0')
continue;
result.symbols.push_back(PdbSymbol{QString::fromUtf8(name), rva});
}
}
qDebug() << "[PDB] extractPdbSymbols:" << result.symbols.size() << "symbols from"
<< result.moduleName;
return result;
}
// ── Public API: enumeratePdbTypes ──
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg) {
@@ -1126,6 +1238,11 @@ NodeTree importPdb(const QString& pdbPath, const QString& structFilter, QString*
namespace rcx {
PdbSymbolResult extractPdbSymbols(const QString&, QString* errorMsg) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
QVector<PdbTypeInfo> enumeratePdbTypes(const QString&, QString* errorMsg) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};

View File

@@ -5,6 +5,25 @@
namespace rcx {
// ── PDB Symbol Extraction ──
struct PdbSymbol {
QString name;
uint32_t rva;
};
struct PdbSymbolResult {
QString moduleName; // derived from PDB filename (e.g. "ntoskrnl")
QVector<PdbSymbol> symbols;
};
// Extract public/global symbols (name → RVA) from a PDB file.
// This reads the DBI stream's public and global symbol sub-streams.
PdbSymbolResult extractPdbSymbols(const QString& pdbPath,
QString* errorMsg = nullptr);
// ── PDB Type Import ──
struct PdbTypeInfo {
uint32_t typeIndex; // TPI type index
QString name; // struct/class/union/enum name

View File

@@ -294,7 +294,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointe
// Defer ref resolution if array references a class
if (!arrayClassName.isEmpty()) {
pendingRefs.append({arrId, arrayClassName});
pendingRefs.push_back(PendingRef{arrId, arrayClassName});
}
childOffset += nodeSize > 0 ? nodeSize : 0;
@@ -321,7 +321,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointe
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, ptrClass});
pendingRefs.push_back(PendingRef{nodeId, ptrClass});
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
continue;
}
@@ -335,7 +335,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointe
if (!n.structTypeName.isEmpty()) {
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, n.structTypeName});
pendingRefs.push_back(PendingRef{nodeId, n.structTypeName});
} else {
tree.addNode(n);
}

View File

@@ -200,10 +200,10 @@ struct Tokenizer {
case '=': tk = TokKind::Equals; break;
default: tk = TokKind::Other; break;
}
tokens.append({tk, QString(c), line});
tokens.push_back(Token{tk, QString(c), line});
pos++;
}
tokens.append({TokKind::Eof, {}, line});
tokens.push_back(Token{TokKind::Eof, {}, line});
}
private:
@@ -241,7 +241,7 @@ private:
bool ok;
int val = m.captured(1).toInt(&ok, 16);
if (ok) {
offsets.append({commentLine, val});
offsets.push_back(LineOffset{commentLine, val});
}
}
}
@@ -259,7 +259,7 @@ private:
void parseIdent() {
int start = pos;
while (pos < src.size() && (src[pos].isLetterOrNumber() || src[pos] == '_')) pos++;
tokens.append({TokKind::Ident, src.mid(start, pos - start), line});
tokens.push_back(Token{TokKind::Ident, src.mid(start, pos - start), line});
}
void parseNumber() {
@@ -276,7 +276,7 @@ private:
// Skip integer suffixes (U, L, LL, ULL, etc.)
while (pos < src.size() && (src[pos] == 'u' || src[pos] == 'U' ||
src[pos] == 'l' || src[pos] == 'L')) pos++;
tokens.append({TokKind::Number, src.mid(start, pos - start), line});
tokens.push_back(Token{TokKind::Number, src.mid(start, pos - start), line});
}
};
@@ -1034,7 +1034,7 @@ struct Parser {
}
}
ps.enumValues.append({memberName, memberValue});
ps.enumValues.emplaceBack(memberName, memberValue);
nextValue = memberValue + 1;
// Skip comma between members
@@ -1312,7 +1312,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
if (!field.pointerTarget.isEmpty() &&
field.pointerTarget != QStringLiteral("void")) {
ctx.pendingRefs.append({nodeId, field.pointerTarget});
ctx.pendingRefs.push_back(PendingRef{nodeId, field.pointerTarget});
}
computedOffset = fieldOffset + ctx.ptrSize;
@@ -1342,7 +1342,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
n.offset = fieldOffset;
int nodeIdx = ctx.tree.addNode(n);
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
ctx.pendingRefs.append({nodeId, field.typeName});
ctx.pendingRefs.push_back(PendingRef{nodeId, field.typeName});
computedOffset = fieldOffset + elemSize;
}
continue;
@@ -1461,7 +1461,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
int nodeIdx = ctx.tree.addNode(n);
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
ctx.pendingRefs.append({nodeId, field.typeName});
ctx.pendingRefs.push_back(PendingRef{nodeId, field.typeName});
if (elemSize > 0)
computedOffset = fieldOffset + totalElements * elemSize;
continue;
@@ -1477,7 +1477,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
int nodeIdx = ctx.tree.addNode(n);
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
ctx.pendingRefs.append({nodeId, field.typeName});
ctx.pendingRefs.push_back(PendingRef{nodeId, field.typeName});
if (elemSize > 0)
computedOffset = fieldOffset + elemSize;
continue;

View File

@@ -0,0 +1,193 @@
#include "pe_debug_info.h"
#include "../providers/provider.h"
#include <cstring>
namespace rcx {
// Minimal PE structures (no Windows SDK dependency)
#pragma pack(push, 1)
struct DosHeader {
uint16_t e_magic; // 'MZ'
uint8_t pad[58];
int32_t e_lfanew; // offset to PE signature
};
struct CoffHeader {
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader;
uint16_t Characteristics;
};
struct DataDirectory {
uint32_t VirtualAddress;
uint32_t Size;
};
// Only the fields we need from the optional header
struct OptionalHeader32 {
uint16_t Magic; // 0x10b = PE32, 0x20b = PE32+
uint8_t pad[90];
uint32_t NumberOfRvaAndSizes;
// DataDirectory[0] = Export, [1] = Import, ... [6] = Debug
};
struct OptionalHeader64 {
uint16_t Magic; // 0x20b = PE32+
uint8_t pad[106];
uint32_t NumberOfRvaAndSizes;
};
struct DebugDirectory {
uint32_t Characteristics;
uint32_t TimeDateStamp;
uint16_t MajorVersion;
uint16_t MinorVersion;
uint32_t Type;
uint32_t SizeOfData;
uint32_t AddressOfRawData; // RVA when loaded
uint32_t PointerToRawData; // file offset (not used for memory reads)
};
struct CvInfoPdb70 {
uint32_t Signature; // 'RSDS'
uint8_t Guid[16];
uint32_t Age;
// char PdbFileName[] follows
};
#pragma pack(pop)
static constexpr uint16_t kMZ = 0x5A4D;
static constexpr uint32_t kPE = 0x00004550;
static constexpr uint16_t kPE32 = 0x10b;
static constexpr uint16_t kPE32P = 0x20b;
static constexpr uint32_t kRSDS = 0x53445352;
static constexpr uint32_t kDebugType_CodeView = 2;
static QString guidToString(const uint8_t guid[16]) {
// Windows GUID is mixed-endian: Data1(4B LE), Data2(2B LE), Data3(2B LE), Data4(8B sequential)
// MS symbol server expects native integer values for Data1/2/3, sequential for Data4
uint32_t d1; memcpy(&d1, guid, 4);
uint16_t d2; memcpy(&d2, guid + 4, 2);
uint16_t d3; memcpy(&d3, guid + 6, 2);
QString s = QStringLiteral("%1%2%3")
.arg(d1, 8, 16, QLatin1Char('0'))
.arg(d2, 4, 16, QLatin1Char('0'))
.arg(d3, 4, 16, QLatin1Char('0'));
for (int i = 8; i < 16; i++)
s += QStringLiteral("%1").arg(guid[i], 2, 16, QLatin1Char('0'));
return s.toUpper();
}
PdbDebugInfo extractPdbDebugInfo(const Provider& prov, uint64_t moduleBase) {
PdbDebugInfo result;
// Read DOS header
DosHeader dos;
if (!prov.read(moduleBase, &dos, sizeof(dos)))
return result;
if (dos.e_magic != kMZ)
return result;
uint64_t peOffset = moduleBase + dos.e_lfanew;
// Read PE signature
uint32_t peSig = 0;
if (!prov.read(peOffset, &peSig, 4))
return result;
if (peSig != kPE)
return result;
// Read COFF header
uint64_t coffOffset = peOffset + 4;
CoffHeader coff;
if (!prov.read(coffOffset, &coff, sizeof(coff)))
return result;
// Read optional header magic to determine PE32 vs PE32+
uint64_t optOffset = coffOffset + sizeof(CoffHeader);
uint16_t optMagic = 0;
if (!prov.read(optOffset, &optMagic, 2))
return result;
// Locate debug data directory (index 6)
uint32_t numRvaAndSizes = 0;
uint64_t dataDirsOffset = 0;
if (optMagic == kPE32) {
// PE32: NumberOfRvaAndSizes at offset 92, data dirs at offset 96
if (!prov.read(optOffset + 92, &numRvaAndSizes, 4))
return result;
dataDirsOffset = optOffset + 96;
} else if (optMagic == kPE32P) {
// PE32+: NumberOfRvaAndSizes at offset 108, data dirs at offset 112
if (!prov.read(optOffset + 108, &numRvaAndSizes, 4))
return result;
dataDirsOffset = optOffset + 112;
} else {
return result;
}
if (numRvaAndSizes <= 6)
return result; // no debug directory
DataDirectory debugDir;
if (!prov.read(dataDirsOffset + 6 * sizeof(DataDirectory), &debugDir, sizeof(debugDir)))
return result;
if (debugDir.VirtualAddress == 0 || debugDir.Size == 0)
return result;
// Read debug directory entries
int numEntries = debugDir.Size / sizeof(DebugDirectory);
for (int i = 0; i < numEntries; i++) {
DebugDirectory entry;
uint64_t entryAddr = moduleBase + debugDir.VirtualAddress + i * sizeof(DebugDirectory);
if (!prov.read(entryAddr, &entry, sizeof(entry)))
continue;
if (entry.Type != kDebugType_CodeView)
continue;
// Read CodeView info (RSDS)
if (entry.AddressOfRawData == 0 || entry.SizeOfData < sizeof(CvInfoPdb70) + 1)
continue;
CvInfoPdb70 cv;
uint64_t cvAddr = moduleBase + entry.AddressOfRawData;
if (!prov.read(cvAddr, &cv, sizeof(cv)))
continue;
if (cv.Signature != kRSDS)
continue;
// Read PDB filename (null-terminated string after the struct)
int nameMaxLen = entry.SizeOfData - sizeof(CvInfoPdb70);
if (nameMaxLen > 260) nameMaxLen = 260;
char nameBuf[261] = {};
if (!prov.read(cvAddr + sizeof(CvInfoPdb70), nameBuf, nameMaxLen))
continue;
nameBuf[nameMaxLen] = '\0';
result.pdbName = QString::fromLatin1(nameBuf);
// Extract just the filename if it contains a path
int lastSlash = result.pdbName.lastIndexOf('\\');
if (lastSlash >= 0)
result.pdbName = result.pdbName.mid(lastSlash + 1);
int lastFwdSlash = result.pdbName.lastIndexOf('/');
if (lastFwdSlash >= 0)
result.pdbName = result.pdbName.mid(lastFwdSlash + 1);
result.guidString = guidToString(cv.Guid);
result.age = cv.Age;
result.valid = true;
return result;
}
return result;
}
} // namespace rcx

View File

@@ -0,0 +1,20 @@
#pragma once
#include <QString>
#include <cstdint>
namespace rcx {
class Provider;
struct PdbDebugInfo {
QString pdbName; // e.g. "ntoskrnl.pdb"
QString guidString; // 32 hex chars, no dashes, uppercase
uint32_t age = 0;
bool valid = false;
};
// Extract PDB debug info (GUID, age, filename) from a PE module in memory.
// Reads DOS header → PE header → debug directory → CodeView RSDS record.
PdbDebugInfo extractPdbDebugInfo(const Provider& prov, uint64_t moduleBase);
} // namespace rcx

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

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
#include "scannerpanel.h"
#include "startpage.h"
#include "workspace_model.h"
namespace rcx { class SymbolDownloader; }
#include <QMainWindow>
#include <QLabel>
#include <QSplitter>
@@ -199,6 +200,28 @@ private:
DockGripWidget* m_scanDockGrip = nullptr;
void createScannerDock();
// Modules/Symbols dock
QDockWidget* m_symbolsDock = nullptr;
QTabWidget* m_symTabWidget = nullptr;
// Modules tab
QTreeView* m_modulesTree = nullptr;
QStandardItemModel* m_modulesModel = nullptr;
// Symbols tab
QTreeView* m_symbolsTree = nullptr;
QStandardItemModel* m_symbolsModel = nullptr;
QSortFilterProxyModel* m_symbolsProxy = nullptr;
QLineEdit* m_symbolsSearch = nullptr;
// Title bar
QLabel* m_symDockTitle = nullptr;
QToolButton* m_symDockCloseBtn = nullptr;
QToolButton* m_symDownloadBtn = nullptr;
DockGripWidget* m_symDockGrip = nullptr;
rcx::SymbolDownloader* m_symDownloader = nullptr;
void createSymbolsDock();
void rebuildSymbolsModel();
void rebuildModulesModel();
void downloadSymbolsForProcess();
// Start page
StartPageWidget* m_startPage = nullptr;
Q_INVOKABLE void showStartPage();

View File

@@ -121,7 +121,7 @@ void McpBridge::onNewConnection() {
auto* pending = m_server->nextPendingConnection();
if (!pending) return;
m_clients.append({pending, {}, false});
m_clients.push_back(ClientState{pending, {}, false});
connect(pending, &QLocalSocket::readyRead,
this, &McpBridge::onReadyRead);
@@ -156,7 +156,7 @@ void McpBridge::onReadyRead() {
if (line.isEmpty()) continue;
if (m_processing) {
m_pendingRequests.append({sock, line});
m_pendingRequests.push_back(PendingRequest{sock, line});
continue;
}
m_processing = true;
@@ -819,7 +819,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
QJsonArray nodeArr;
struct QueueEntry { uint64_t parentId; int depth; };
QVector<QueueEntry> queue;
queue.append({filterParentId, 0});
queue.push_back(QueueEntry{filterParentId, 0});
int totalCount = 0; // total nodes that match depth filter
int emitted = 0;
@@ -839,13 +839,13 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
if (totalCount <= offset) {
// Still skipping — but enqueue children for counting
if (entry.depth + 1 <= maxDepth)
queue.append({n.id, entry.depth + 1});
queue.push_back(QueueEntry{n.id, entry.depth + 1});
continue;
}
if (emitted >= limit) {
// Past limit — just keep counting total
if (entry.depth + 1 <= maxDepth)
queue.append({n.id, entry.depth + 1});
queue.push_back(QueueEntry{n.id, entry.depth + 1});
continue;
}
@@ -875,7 +875,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
// Enqueue children if we haven't hit depth limit
if (entry.depth + 1 <= maxDepth)
queue.append({n.id, entry.depth + 1});
queue.push_back(QueueEntry{n.id, entry.depth + 1});
}
}
@@ -1665,7 +1665,7 @@ static QVector<AddressRange> parseRegionsArg(const QJsonObject& args, QString* e
if (errOut) *errOut = QStringLiteral("regions[%1]: end must be > start").arg(i);
return {};
}
out.append({start, end});
out.push_back(AddressRange{start, end});
}
return out;
}

View File

@@ -41,6 +41,11 @@ void PluginManager::LoadPlugins()
for (const QFileInfo& fileInfo : files)
{
// Skip the remote-inject payload binary — it's not a plugin and
// loading it (especially on Linux) spawns a rogue thread.
if (fileInfo.baseName().startsWith("rcx_payload"))
continue;
LoadPlugin(fileInfo.absoluteFilePath());
}
@@ -83,7 +88,7 @@ bool PluginManager::LoadPlugin(const QString& path)
qDebug() << "PluginManager: Loaded plugin:" << plugin->Name().c_str() << plugin->Version().c_str() << "by" << plugin->Author().c_str();
// Store plugin entry
m_entries.append({library, plugin});
m_entries.push_back(PluginEntry{library, plugin});
m_plugins.append(plugin);
// Auto-register providers in global registry

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,22 @@ public:
struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; };
virtual QVector<ThreadInfo> tebs() const { return {}; }
struct ModuleEntry { QString name; QString fullPath; uint64_t base; uint64_t size; };
virtual QVector<ModuleEntry> enumerateModules() 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

@@ -1,241 +1,173 @@
#pragma once
#include "themes/thememanager.h"
#include <QWidget>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QScreen>
#include <QTimer>
#include <QPropertyAnimation>
#include <QCursor>
#include <cstdio>
#define TIP_LOG(...) do { \
FILE* _f = fopen("E:/game_dev/util/reclass2027-main/build/tip_trace.log", "a"); \
if (_f) { fprintf(_f, __VA_ARGS__); fclose(_f); } \
} while(0)
#include <QApplication>
#include <QMouseEvent>
#include <functional>
namespace rcx {
// ── Modern arrow tooltip ──
// Draws a rounded-rect body with a triangular arrow whose tip touches
// the anchor point (center of the dwell area).
//
// Bypasses Fusion/CSS/DWM entirely — everything is manual QPainter on a
// WA_TranslucentBackground layered window. The DarkTitleBar property is
// pre-set to prevent DarkApp::notify from calling DwmSetWindowAttribute
// (which was the root cause of the previous transparent-window failure).
//
// Usage:
// tip->setTheme(bg, border, titleCol, bodyCol, sepCol);
// tip->populate("Title", "line1\nline2", font);
// tip->showAt(QPoint(midX, lineBottom)); // arrow tip at this point
// tip->dismiss();
class RcxTooltip : public QWidget {
public:
static RcxTooltip* instance() {
static RcxTooltip* s = nullptr;
if (!s) {
s = new RcxTooltip;
QObject::connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
s, [](const rcx::Theme&) { /* colors read live in paintEvent */ });
}
return s;
static constexpr int kArrowH = 8;
static constexpr int kArrowW = 14;
static constexpr int kRadius = 6;
static constexpr int kPad = 10;
static constexpr int kGap = 4;
static constexpr int kMaxW = 550;
std::function<void(QMouseEvent*)> onMouseMove;
explicit RcxTooltip(QWidget* parent = nullptr)
: QWidget(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
// ── Key fix: prevent DwmSetWindowAttribute on this window ──
// DarkApp::notify checks this property and skips DWM calls.
// Without this, DWMWA_USE_IMMERSIVE_DARK_MODE breaks WS_EX_LAYERED
// alpha compositing on Windows 10/11.
setProperty("DarkTitleBar", true);
setAttribute(Qt::WA_TranslucentBackground);
setAttribute(Qt::WA_ShowWithoutActivating);
setAttribute(Qt::WA_DeleteOnClose, false);
setMouseTracking(true);
}
void showFor(QWidget* trigger, const QString& text) {
if (!trigger || text.isEmpty()) {
TIP_LOG("[TIP] showFor: null trigger or empty text -- dismiss\n");
dismiss(); return;
}
void setTheme(const QColor& bg, const QColor& border,
const QColor& title, const QColor& body, const QColor& sep) {
m_bg = bg; m_border = border;
m_titleCol = title; m_bodyCol = body; m_sepCol = sep;
}
// Same widget+text already showing — do nothing (prevents teleport)
if (m_trigger == trigger && m_text == text && isVisible()) {
TIP_LOG("[TIP] showFor: same widget+text, already visible -- skip\n");
return;
}
void populate(const QString& title, const QString& body, const QFont& font) {
if (title == m_title && body == m_body && isVisible()) return;
m_title = title; m_body = body;
m_lines = body.split('\n');
m_font = font;
m_font.setPointSizeF(font.pointSizeF() * 0.9);
m_bold = m_font; m_bold.setBold(true);
recalc();
}
TIP_LOG("[TIP] showFor: text='%s' trigger=%p class=%s\n",
qPrintable(text), (void*)trigger, trigger->metaObject()->className());
// Cancel pending dismiss
if (m_dismissTimer) m_dismissTimer->stop();
m_trigger = trigger;
m_text = text;
m_label->setText(text);
m_label->adjustSize();
// ── Size: label + padding + arrow ──
const int pad = 8;
const int vpad = 4;
int bodyW = m_label->sizeHint().width() + pad * 2;
int bodyH = m_label->sizeHint().height() + vpad * 2;
int totalW = bodyW;
int totalH = bodyH + kArrowH;
// ── Position relative to trigger widget ──
QRect trigGlobal = QRect(trigger->mapToGlobal(QPoint(0, 0)), trigger->size());
int trigCenterX = trigGlobal.center().x();
QScreen* screen = QApplication::screenAt(trigGlobal.center());
QRect scr = screen ? screen->availableGeometry() : QRect(0, 0, 1920, 1080);
// Default: above the trigger
m_arrowDown = true;
int x = trigCenterX - totalW / 2;
int y = trigGlobal.top() - totalH - kGap;
// Flip below if not enough room above
if (y < scr.top()) {
m_arrowDown = false;
y = trigGlobal.bottom() + kGap;
}
// Clamp horizontally
if (x < scr.left()) x = scr.left() + 2;
if (x + totalW > scr.right()) x = scr.right() - totalW - 2;
// Arrow X in local coords
m_arrowLocalX = trigCenterX - x;
m_arrowLocalX = qBound(kArrowHalfW + 4, m_arrowLocalX, totalW - kArrowHalfW - 4);
// Position label inside the body
if (m_arrowDown)
m_label->move(pad, vpad);
else
m_label->move(pad, kArrowH + vpad);
m_bodyRect = m_arrowDown
? QRect(0, 0, bodyW, bodyH)
: QRect(0, kArrowH, bodyW, bodyH);
setFixedSize(totalW, totalH);
// `anchor`: global screen point where the arrow tip touches.
// Typically the center-bottom of the hovered span.
void showAt(const QPoint& anchor) {
QRect scr = screenAt(anchor);
int w = m_bw, h = m_bh + kArrowH;
m_up = (anchor.y() + h <= scr.bottom());
int x = qBound(scr.left() + 2, anchor.x() - w / 2, scr.right() - w - 2);
int y = m_up ? anchor.y() : anchor.y() - h;
m_ax = qBound(kRadius + kArrowW/2 + 1, anchor.x() - x,
w - kRadius - kArrowW/2 - 1);
setFixedSize(w, h);
move(x, y);
if (!isVisible()) {
TIP_LOG("[TIP] showFor: showing at (%d,%d) size=%dx%d arrowDown=%d arrowX=%d\n",
x, y, totalW, totalH, m_arrowDown, m_arrowLocalX);
setWindowOpacity(0.0);
show();
raise();
// Fade in
auto* anim = new QPropertyAnimation(this, "windowOpacity", this);
anim->setDuration(80);
anim->setStartValue(0.0);
anim->setEndValue(1.0);
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->start(QAbstractAnimation::DeleteWhenStopped);
} else {
TIP_LOG("[TIP] showFor: already visible, updating\n");
update();
}
if (!isVisible()) show();
update();
}
void dismiss() {
TIP_LOG("[TIP] dismiss: wasVisible=%d\n", isVisible());
if (m_dismissTimer) m_dismissTimer->stop();
if (isVisible()) hide();
m_trigger = nullptr;
}
// Schedule dismiss with a delay — but only if the cursor has truly
// left the trigger+tooltip zone. Qt fires synthetic Leave events
// when a tooltip window appears above the trigger; we must ignore those.
void scheduleDismiss() {
if (m_trigger) {
QPoint cursor = QCursor::pos();
QRect trigRect(m_trigger->mapToGlobal(QPoint(0, 0)), m_trigger->size());
QRect tipRect(pos(), size());
QRect zone = trigRect.united(tipRect).adjusted(-4, -4, 4, 4);
bool inside = zone.contains(cursor);
TIP_LOG("[TIP] scheduleDismiss: cursor=(%d,%d) zone=(%d,%d %dx%d) inside=%d\n",
cursor.x(), cursor.y(),
zone.x(), zone.y(), zone.width(), zone.height(), inside);
if (inside)
return; // cursor still inside — ignore spurious Leave
}
if (!m_dismissTimer) {
m_dismissTimer = new QTimer(this);
m_dismissTimer->setSingleShot(true);
connect(m_dismissTimer, &QTimer::timeout, this, &RcxTooltip::dismiss);
}
m_dismissTimer->start(100);
}
QWidget* currentTrigger() const { return m_trigger; }
// ── Geometry accessors (for testing) ──
bool arrowPointsDown() const { return m_arrowDown; }
int arrowLocalX() const { return m_arrowLocalX; }
QRect bodyRect() const { return m_bodyRect; }
QString currentText() const { return m_text; }
// Constants exposed for testing
static constexpr int kArrowH = 6;
static constexpr int kArrowHalfW = 6;
static constexpr int kGap = 2;
void dismiss() { if (isVisible()) hide(); }
protected:
void paintEvent(QPaintEvent*) override {
TIP_LOG("[TIP] paintEvent: size=%dx%d bodyRect=(%d,%d %dx%d)\n",
width(), height(),
m_bodyRect.x(), m_bodyRect.y(), m_bodyRect.width(), m_bodyRect.height());
const auto& theme = ThemeManager::instance().current();
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
// Fill entire widget with the tooltip background first
// (no WA_TranslucentBackground, so unpainted areas would be opaque garbage)
p.fillRect(rect(), theme.backgroundAlt);
// Body rect (excludes arrow space)
QRectF b(0.5, m_up ? kArrowH + 0.5 : 0.5,
width() - 1.0, m_bh - 1.0);
qreal r = kRadius, ax = m_ax, ah = kArrowW / 2.0;
// Build path: rounded body + triangle arrow
QPainterPath path;
path.addRoundedRect(QRectF(m_bodyRect), 4.0, 4.0);
// Triangle arrow
QPolygonF arrow;
if (m_arrowDown) {
int ay = m_bodyRect.bottom();
arrow << QPointF(m_arrowLocalX - kArrowHalfW, ay)
<< QPointF(m_arrowLocalX, ay + kArrowH)
<< QPointF(m_arrowLocalX + kArrowHalfW, ay);
} else {
int ay = kArrowH;
arrow << QPointF(m_arrowLocalX - kArrowHalfW, ay)
<< QPointF(m_arrowLocalX, 0)
<< QPointF(m_arrowLocalX + kArrowHalfW, ay);
// ── Single contiguous path: rounded rect + arrow notch ──
// No QPainterPath::united() — that causes junction artifacts.
// Clockwise from top-left, inserting the arrow inline.
QPainterPath pp;
pp.moveTo(b.left() + r, b.top());
if (m_up) {
pp.lineTo(ax - ah, b.top());
pp.lineTo(ax, 0.5);
pp.lineTo(ax + ah, b.top());
}
QPainterPath arrowPath;
arrowPath.addPolygon(arrow);
arrowPath.closeSubpath();
path = path.united(arrowPath);
pp.lineTo(b.right() - r, b.top());
pp.arcTo(b.right() - 2*r, b.top(), 2*r, 2*r, 90, -90);
pp.lineTo(b.right(), b.bottom() - r);
pp.arcTo(b.right() - 2*r, b.bottom() - 2*r, 2*r, 2*r, 0, -90);
if (!m_up) {
pp.lineTo(ax + ah, b.bottom());
pp.lineTo(ax, height() - 0.5);
pp.lineTo(ax - ah, b.bottom());
}
pp.lineTo(b.left() + r, b.bottom());
pp.arcTo(b.left(), b.bottom() - 2*r, 2*r, 2*r, 270, -90);
pp.lineTo(b.left(), b.top() + r);
pp.arcTo(b.left(), b.top(), 2*r, 2*r, 180, -90);
pp.closeSubpath();
// Stroke the shape border
p.setPen(QPen(theme.border, 1.0));
p.setBrush(theme.backgroundAlt);
p.drawPath(path);
p.setPen(QPen(m_border, 1));
p.setBrush(m_bg);
p.drawPath(pp);
// ── Content: title + separator + body ──
qreal cy = (m_up ? kArrowH : 0) + kPad;
QFontMetrics tf(m_bold), bf(m_font);
if (!m_title.isEmpty()) {
p.setFont(m_bold); p.setPen(m_titleCol);
p.drawText(QPointF(kPad, cy + tf.ascent()), m_title);
cy += tf.height() + kGap;
p.setPen(m_sepCol);
p.drawLine(QPointF(kPad, cy), QPointF(width() - kPad, cy));
cy += 1 + kGap;
}
p.setFont(m_font); p.setPen(m_bodyCol);
for (const auto& l : m_lines) {
p.drawText(QPointF(kPad, cy + bf.ascent()), l);
cy += bf.lineSpacing();
}
}
void mouseMoveEvent(QMouseEvent* e) override {
if (onMouseMove) onMouseMove(e); else QWidget::mouseMoveEvent(e);
}
private:
explicit RcxTooltip()
: QWidget(nullptr, Qt::ToolTip | Qt::FramelessWindowHint)
{
// NOTE: WA_TranslucentBackground removed — it breaks under DWM dark mode
// (DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE kills layered compositing)
setAttribute(Qt::WA_ShowWithoutActivating);
setAutoFillBackground(false); // we paint everything ourselves in paintEvent
m_label = new QLabel(this);
m_label->setAlignment(Qt::AlignCenter);
updateLabelStyle();
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
this, [this](const rcx::Theme&) { updateLabelStyle(); });
static QRect screenAt(const QPoint& pt) {
auto* s = QApplication::screenAt(pt);
return s ? s->availableGeometry() : QRect(0, 0, 1920, 1080);
}
void updateLabelStyle() {
const auto& theme = ThemeManager::instance().current();
m_label->setStyleSheet(
QStringLiteral("QLabel { color: %1; background: transparent; padding: 0; }")
.arg(theme.text.name()));
void recalc() {
QFontMetrics tf(m_bold), bf(m_font);
int maxW = m_title.isEmpty() ? 0 : tf.horizontalAdvance(m_title);
for (const auto& l : m_lines) maxW = qMax(maxW, bf.horizontalAdvance(l));
m_bw = qMin(maxW + 2 * kPad, kMaxW);
m_bh = kPad + (m_title.isEmpty() ? 0 : tf.height() + kGap + 1 + kGap)
+ m_lines.size() * bf.lineSpacing() + kPad;
}
QLabel* m_label = nullptr;
QWidget* m_trigger = nullptr;
QString m_text;
QTimer* m_dismissTimer = nullptr;
bool m_arrowDown = true;
int m_arrowLocalX = 0;
QRect m_bodyRect;
QString m_title, m_body;
QStringList m_lines;
QFont m_font, m_bold;
QColor m_bg{30, 30, 30}, m_border{60, 60, 60};
QColor m_titleCol{220, 220, 220}, m_bodyCol{180, 180, 180}, m_sepCol{60, 60, 60};
bool m_up = true;
int m_ax = 0, m_bw = 0, m_bh = 0;
};
} // namespace rcx

View File

@@ -17,6 +17,7 @@
<file alias="file-binary.svg">vsicons/file-binary.svg</file>
<file alias="debug.svg">vsicons/debug.svg</file>
<file alias="close.svg">vsicons/close.svg</file>
<file alias="cloud-download.svg">vsicons/cloud-download.svg</file>
<file alias="arrow-left.svg">vsicons/arrow-left.svg</file>
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
<file alias="split-horizontal.svg">vsicons/split-horizontal.svg</file>
@@ -55,6 +56,7 @@
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
<file alias="symbol-class.svg">vsicons/symbol-class.svg</file>
<file alias="symbol-variable.svg">vsicons/symbol-variable.svg</file>
<file alias="symbol-method.svg">vsicons/symbol-method.svg</file>
<file alias="server-process.svg">vsicons/server-process.svg</file>
<file alias="remote.svg">vsicons/remote.svg</file>
<file alias="plug.svg">vsicons/plug.svg</file>

View File

@@ -171,8 +171,8 @@ private:
for (const auto& path : s.value("recentFiles").toStringList()) {
QFileInfo fi(path);
if (!fi.exists()) continue;
m_all.append({fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
fi.lastModified(), false});
m_all.push_back(Entry{fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
fi.lastModified(), false});
}
#ifdef __APPLE__
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
@@ -180,8 +180,8 @@ private:
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
#endif
for (const auto& fn : exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name))
m_all.append({exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
QFileInfo(exDir.filePath(fn)).lastModified(), true});
m_all.push_back(Entry{exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
QFileInfo(exDir.filePath(fn)).lastModified(), true});
}
void buildGroups() {
@@ -207,7 +207,7 @@ private:
static const char* names[] = {"Today","Yesterday","This week","This month","Older","Examples"};
m_groups.clear();
for (int i = 0; i < 6; i++)
if (!bk[i].isEmpty()) m_groups.append({names[i], true, bk[i]});
if (!bk[i].isEmpty()) m_groups.push_back(Group{names[i], true, bk[i]});
m_scrollY = 0;
}
@@ -223,13 +223,11 @@ private:
{":/vsicons/debug.svg", "Import PDB", "Import types from a .pdb symbol file"}
};
const int N = 5, CH = 84, R = 6, panelH = N * CH;
const int N = 5, CH = 84, panelH = N * CH;
// Rounded panel background
QPainterPath clip;
clip.addRoundedRect(QRectF(x, y, w, panelH), R, R);
// Sharp-cornered panel background
p.save();
p.setClipPath(clip);
p.setClipRect(QRectF(x, y, w, panelH));
p.fillRect(x, y, w, panelH, m_t.background);
for (int i = 0; i < N; i++) {
@@ -289,7 +287,7 @@ private:
if (gi > 0) fy += 15;
// Group header
m_grpRects.append({gi, QRectF(x, fy, w, 28)});
m_grpRects.emplaceBack(gi, QRectF(x, fy, w, 28));
p.setPen(Qt::NoPen); p.setBrush(m_t.text);
int triX = x + 8, triY = fy + 11;
QPolygonF tri;
@@ -307,7 +305,7 @@ private:
for (int ei : g.entries) {
auto& e = m_filtered[ei];
QRectF er(x, fy, w, 52);
m_entRects.append({ei, er});
m_entRects.emplaceBack(ei, er);
if (m_hz == HZ_Entry && m_hi == ei) p.fillRect(er, m_t.hover);
drawIcon(p, e.isExample ? ":/vsicons/book.svg" : ":/vsicons/symbol-structure.svg",

123
src/symbol_downloader.cpp Normal file
View File

@@ -0,0 +1,123 @@
#include "symbol_downloader.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QUrl>
namespace rcx {
SymbolDownloader::SymbolDownloader(QObject* parent)
: QObject(parent)
, m_nam(new QNetworkAccessManager(this))
{
}
QString SymbolDownloader::cacheDir() {
QString base = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
return base + QStringLiteral("/SymbolCache");
}
QString SymbolDownloader::findCached(const DownloadRequest& req) const {
// Cache layout: cacheDir/pdbName/GUID+age/pdbName
QString path = cacheDir() + QStringLiteral("/%1/%2%3/%1")
.arg(req.pdbName, req.guidString, QString::number(req.age, 16));
if (QFile::exists(path))
return path;
return {};
}
QString SymbolDownloader::findLocal(const QString& moduleFullPath, const QString& pdbName) {
if (moduleFullPath.isEmpty() || pdbName.isEmpty())
return {};
// Check same directory as the module
QString dir = QFileInfo(moduleFullPath).absolutePath();
QString candidate = dir + QStringLiteral("/") + pdbName;
if (QFile::exists(candidate))
return candidate;
return {};
}
void SymbolDownloader::download(const DownloadRequest& req) {
// URL: https://msdl.microsoft.com/download/symbols/{pdbName}/{GUID}{age}/{pdbName}
QString url = QStringLiteral("https://msdl.microsoft.com/download/symbols/%1/%2%3/%1")
.arg(req.pdbName, req.guidString, QString::number(req.age, 16));
QUrl reqUrl(url);
QNetworkRequest netReq(reqUrl);
netReq.setHeader(QNetworkRequest::UserAgentHeader,
QStringLiteral("Microsoft-Symbol-Server/10.0.0.0"));
netReq.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
cancel(); // cancel any previous
m_activeReply = m_nam->get(netReq);
QString moduleName = req.moduleName;
QString pdbName = req.pdbName;
QString guidString = req.guidString;
uint32_t age = req.age;
connect(m_activeReply, &QNetworkReply::downloadProgress,
this, [this, moduleName](qint64 received, qint64 total) {
emit progress(moduleName, static_cast<int>(received), static_cast<int>(total));
});
connect(m_activeReply, &QNetworkReply::finished,
this, [this, moduleName, pdbName, guidString, age]() {
auto* reply = m_activeReply;
m_activeReply = nullptr;
if (!reply) return;
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit finished(moduleName, {}, false,
QStringLiteral("Download failed: %1").arg(reply->errorString()));
return;
}
int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (httpStatus != 200) {
emit finished(moduleName, {}, false,
QStringLiteral("HTTP %1").arg(httpStatus));
return;
}
QByteArray data = reply->readAll();
if (data.isEmpty()) {
emit finished(moduleName, {}, false, QStringLiteral("Empty response"));
return;
}
// Save to cache
QString dir = cacheDir() + QStringLiteral("/%1/%2%3")
.arg(pdbName, guidString, QString::number(age, 16));
QDir().mkpath(dir);
QString path = dir + QStringLiteral("/") + pdbName;
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
emit finished(moduleName, {}, false,
QStringLiteral("Cannot write: %1").arg(f.errorString()));
return;
}
f.write(data);
f.close();
emit finished(moduleName, path, true, {});
});
}
void SymbolDownloader::cancel() {
if (m_activeReply) {
m_activeReply->abort();
m_activeReply->deleteLater();
m_activeReply = nullptr;
}
}
} // namespace rcx

50
src/symbol_downloader.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include <QObject>
#include <QString>
#include <QVector>
#include <cstdint>
class QNetworkAccessManager;
class QNetworkReply;
namespace rcx {
class SymbolDownloader : public QObject {
Q_OBJECT
public:
explicit SymbolDownloader(QObject* parent = nullptr);
struct DownloadRequest {
QString moduleName; // display name (e.g. "ntoskrnl.exe")
QString pdbName; // PDB filename (e.g. "ntoskrnl.pdb")
QString guidString; // 32 hex chars, no dashes
uint32_t age = 0;
};
// Check if PDB exists in local cache. Returns path or empty.
QString findCached(const DownloadRequest& req) const;
// Check if PDB exists next to the module on disk. Returns path or empty.
static QString findLocal(const QString& moduleFullPath, const QString& pdbName);
// Start downloading a PDB from MS symbol server.
// Emits finished() when done (success or failure).
void download(const DownloadRequest& req);
// Cancel any in-progress download.
void cancel();
// Local symbol cache directory.
static QString cacheDir();
signals:
void progress(const QString& moduleName, int bytesReceived, int bytesTotal);
void finished(const QString& moduleName, const QString& localPath,
bool success, const QString& error);
private:
QNetworkAccessManager* m_nam = nullptr;
QNetworkReply* m_activeReply = nullptr;
};
} // namespace rcx

171
src/symbolstore.cpp Normal file
View File

@@ -0,0 +1,171 @@
#include "symbolstore.h"
#include "providers/provider.h"
#include <QDebug>
namespace rcx {
uint64_t SymbolStore::getModuleBase(const Provider* provider, const QString& canonical) const {
if (!provider)
return 0;
uint64_t base = provider->symbolToAddress(canonical);
if (base == 0)
base = provider->symbolToAddress(canonical + QStringLiteral(".exe"));
if (base == 0)
base = provider->symbolToAddress(canonical + QStringLiteral(".dll"));
if (base == 0)
base = provider->symbolToAddress(canonical + QStringLiteral(".sys"));
return base;
}
int SymbolStore::addModule(const QString& moduleName, const QString& pdbPath,
const QVector<QPair<QString, uint32_t>>& symbols) {
QString canonical = resolveAlias(moduleName);
PdbSymbolSet set;
set.pdbPath = pdbPath;
set.moduleName = canonical;
set.nameToRva.reserve(symbols.size());
set.rvaToName.reserve(symbols.size());
for (const auto& sym : symbols) {
if (set.nameToRva.contains(sym.first))
continue;
set.nameToRva.insert(sym.first, sym.second);
set.rvaToName.emplaceBack(sym.second, sym.first);
}
set.sortRvaIndex();
int count = set.nameToRva.size();
// Register the raw module name as an alias if it differs from canonical
QString rawLower = moduleName.toLower();
if (rawLower.endsWith(QStringLiteral(".exe")) || rawLower.endsWith(QStringLiteral(".dll")) ||
rawLower.endsWith(QStringLiteral(".sys")))
rawLower = rawLower.left(rawLower.lastIndexOf('.'));
if (rawLower != canonical)
m_aliases[rawLower] = canonical;
m_modules[canonical] = std::move(set);
qDebug() << "[SymbolStore] loaded" << count << "symbols for module" << canonical
<< "(from" << pdbPath << ")";
return count;
}
void SymbolStore::unloadModule(const QString& moduleName) {
QString canonical = resolveAlias(moduleName);
m_modules.remove(canonical);
}
uint64_t SymbolStore::resolve(const QString& token, const Provider* provider, bool* ok) const {
*ok = false;
// Check for "module!symbol" syntax
int bangIdx = token.indexOf('!');
if (bangIdx > 0 && bangIdx < token.size() - 1) {
QString modPart = token.left(bangIdx);
QString symPart = token.mid(bangIdx + 1);
QString canonical = resolveAlias(modPart);
auto modIt = m_modules.find(canonical);
if (modIt == m_modules.end())
return 0;
auto symIt = modIt->nameToRva.find(symPart);
if (symIt == modIt->nameToRva.end())
return 0;
uint32_t rva = *symIt;
uint64_t moduleBase = getModuleBase(provider, canonical);
// Also try the user-supplied module name form
if (moduleBase == 0)
moduleBase = getModuleBase(provider, modPart);
*ok = true;
return moduleBase + rva;
}
// Bare symbol — search all loaded modules
uint32_t foundRva = 0;
QString foundModule;
int matches = 0;
for (auto it = m_modules.begin(); it != m_modules.end(); ++it) {
auto symIt = it->nameToRva.find(token);
if (symIt != it->nameToRva.end()) {
foundRva = *symIt;
foundModule = it.key();
matches++;
if (matches > 1)
return 0; // ambiguous
}
}
if (matches == 1) {
uint64_t moduleBase = getModuleBase(provider, foundModule);
*ok = true;
return moduleBase + foundRva;
}
// Fallback: treat bare token as a module name (e.g. "ntdll" → ntdll base)
if (matches == 0) {
QString canonical = resolveAlias(token);
uint64_t moduleBase = getModuleBase(provider, canonical);
if (moduleBase != 0) {
*ok = true;
return moduleBase;
}
}
return 0;
}
QString SymbolStore::getSymbolForAddress(uint64_t addr, const Provider* provider) const {
if (m_modules.isEmpty() || !provider)
return {};
for (auto it = m_modules.begin(); it != m_modules.end(); ++it) {
const PdbSymbolSet& set = *it;
uint64_t moduleBase = getModuleBase(provider, set.moduleName);
if (moduleBase == 0)
continue;
if (addr < moduleBase)
continue;
uint32_t rva = static_cast<uint32_t>(addr - moduleBase);
if (set.rvaToName.isEmpty())
continue;
// Binary search: find last entry with RVA <= target
auto upper = std::upper_bound(set.rvaToName.begin(), set.rvaToName.end(), rva,
[](uint32_t val, const QPair<uint32_t, QString>& entry) {
return val < entry.first;
});
if (upper == set.rvaToName.begin())
continue;
--upper;
uint32_t displacement = rva - upper->first;
static constexpr uint32_t kMaxDisplacement = 0x1000;
if (displacement > kMaxDisplacement)
continue;
if (displacement == 0)
return set.moduleName + QStringLiteral("!") + upper->second;
return set.moduleName + QStringLiteral("!") + upper->second
+ QStringLiteral("+0x") + QString::number(displacement, 16);
}
return {};
}
void SymbolStore::addAlias(const QString& alias, const QString& canonicalModule) {
m_aliases[alias.toLower()] = canonicalModule.toLower();
}
} // namespace rcx

95
src/symbolstore.h Normal file
View File

@@ -0,0 +1,95 @@
#pragma once
#include <QString>
#include <QStringList>
#include <QHash>
#include <QVector>
#include <QPair>
#include <algorithm>
namespace rcx {
class Provider; // forward declaration
struct PdbSymbolSet {
QString pdbPath;
QString moduleName; // canonical lowercase name (e.g. "ntoskrnl")
QHash<QString, uint32_t> nameToRva;
QVector<QPair<uint32_t, QString>> rvaToName; // sorted by RVA for binary search
void sortRvaIndex() {
std::sort(rvaToName.begin(), rvaToName.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
}
};
class SymbolStore {
public:
static SymbolStore& instance() {
static SymbolStore s;
return s;
}
// Add a pre-extracted symbol set for a module.
// moduleName is the canonical name (e.g. "ntoskrnl").
// Returns the number of unique symbols stored.
int addModule(const QString& moduleName, const QString& pdbPath,
const QVector<QPair<QString, uint32_t>>& symbols);
// Unload symbols for a module.
void unloadModule(const QString& moduleName);
// Resolve a token from the expression parser.
// Handles "module!symbol" (qualified) and bare "symbol" (unqualified).
// Uses provider->symbolToAddress() to get the module's runtime base address.
uint64_t resolve(const QString& token, const Provider* provider, bool* ok) const;
// Reverse lookup: given an absolute address and a provider, find the nearest symbol.
// Returns "module!symbol" or "module!symbol+0xN", or empty if no match.
QString getSymbolForAddress(uint64_t addr, const Provider* provider) const;
// Check if any symbols are loaded.
bool hasSymbols() const { return !m_modules.isEmpty(); }
// List loaded module names.
QStringList loadedModules() const { return m_modules.keys(); }
// Number of loaded modules.
int moduleCount() const { return m_modules.size(); }
// Access module data by name (returns nullptr if not found).
const PdbSymbolSet* moduleData(const QString& moduleName) const {
QString canonical = resolveAlias(moduleName);
auto it = m_modules.find(canonical);
return it != m_modules.end() ? &*it : nullptr;
}
// Add a module alias (e.g. "nt" → "ntoskrnl").
void addAlias(const QString& alias, const QString& canonicalModule);
// Resolve alias to canonical module name (public for callers that need it)
QString resolveAlias(const QString& name) const {
QString lower = name.toLower();
if (lower.endsWith(QStringLiteral(".exe")) || lower.endsWith(QStringLiteral(".dll")) ||
lower.endsWith(QStringLiteral(".sys")))
lower = lower.left(lower.lastIndexOf('.'));
auto it = m_aliases.find(lower);
return it != m_aliases.end() ? *it : lower;
}
private:
SymbolStore() {
// Common Windows kernel aliases
m_aliases[QStringLiteral("nt")] = QStringLiteral("ntoskrnl");
m_aliases[QStringLiteral("ntkrnlmp")] = QStringLiteral("ntoskrnl");
m_aliases[QStringLiteral("ntkrnlpa")] = QStringLiteral("ntoskrnl");
m_aliases[QStringLiteral("ntkrpamp")] = QStringLiteral("ntoskrnl");
}
// Get the module base address, trying various name forms
uint64_t getModuleBase(const Provider* provider, const QString& canonical) const;
QHash<QString, PdbSymbolSet> m_modules; // canonical lowercase name → symbol set
QHash<QString, QString> m_aliases; // alias → canonical name
};
} // namespace rcx

View File

@@ -4,6 +4,7 @@
#include <QMouseEvent>
#include <QPainter>
#include <QStyle>
#include <QTimer>
#include <QWindow>
namespace rcx {
@@ -25,11 +26,23 @@ TitleBarWidget::TitleBarWidget(QWidget* parent)
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
layout->addWidget(m_appLabel);
// Menu bar
// Menu bar — hidden on Linux; visible on Windows.
// On Linux, QMenuBar inside a custom widget collapses all items into an
// extension popup. We keep it hidden and mirror its menus as QToolButtons
// via finalizeMenuBar() after createMenus() populates it.
m_menuBar = new QMenuBar(this);
m_menuBar->setNativeMenuBar(false);
#ifdef __linux__
m_useToolButtons = true;
m_menuBar->hide();
m_menuBtnLayout = new QHBoxLayout;
m_menuBtnLayout->setContentsMargins(0, 0, 0, 0);
m_menuBtnLayout->setSpacing(0);
layout->addLayout(m_menuBtnLayout);
#else
m_menuBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
layout->addWidget(m_menuBar);
#endif
layout->addStretch();
@@ -116,6 +129,17 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
m_btnMin->setStyleSheet(btnStyle);
m_btnMax->setStyleSheet(btnStyle);
// Linux menu tool buttons
if (m_useToolButtons) {
QString menuBtnStyle = QStringLiteral(
"QToolButton { background: transparent; border: none; padding: 0 8px; color: %1; }"
"QToolButton:hover { background: %2; }"
"QToolButton::menu-indicator { image: none; }")
.arg(theme.text.name(), theme.hover.name());
for (auto* btn : m_menuButtons)
btn->setStyleSheet(menuBtnStyle);
}
// Close button: themed red hover
m_btnClose->setStyleSheet(QStringLiteral(
"QToolButton { background: transparent; border: none; }"
@@ -164,6 +188,58 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
action->setText("&" + result);
}
}
// Sync tool button labels on Linux
if (m_useToolButtons) {
auto actions = m_menuBar->actions();
for (int i = 0; i < m_menuButtons.size() && i < actions.size(); ++i)
m_menuButtons[i]->setText(actions[i]->text());
}
}
void TitleBarWidget::finalizeMenuBar() {
if (!m_useToolButtons) return;
// Create a QToolButton for each top-level menu in the hidden QMenuBar
for (auto* action : m_menuBar->actions()) {
if (!action->menu()) continue;
auto* btn = new QToolButton(this);
btn->setText(action->text());
btn->setMenu(action->menu());
btn->setPopupMode(QToolButton::InstantPopup);
btn->setAutoRaise(true);
btn->setFocusPolicy(Qt::NoFocus);
btn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
btn->setStyleSheet(QStringLiteral(
"QToolButton { background: transparent; border: none; padding: 0 8px; }"
"QToolButton:hover { background: %1; }"
"QToolButton::menu-indicator { image: none; }")
.arg(m_theme.hover.name()));
btn->installEventFilter(this);
btn->menu()->installEventFilter(this);
m_menuBtnLayout->addWidget(btn);
m_menuButtons.append(btn);
}
}
bool TitleBarWidget::eventFilter(QObject* obj, QEvent* event) {
if (!m_useToolButtons) return QWidget::eventFilter(obj, event);
// Watch for mouse movement inside an open QMenu — if the cursor moves
// over a sibling menu button, close this menu and open the other.
if (event->type() == QEvent::MouseMove) {
auto* menu = qobject_cast<QMenu*>(obj);
if (!menu || !menu->isVisible()) return false;
QPoint globalPos = QCursor::pos();
for (auto* btn : m_menuButtons) {
if (btn->menu() == menu) continue;
QRect btnRect(btn->mapToGlobal(QPoint(0, 0)), btn->size());
if (btnRect.contains(globalPos)) {
menu->close();
QTimer::singleShot(0, btn, [btn]() { btn->showMenu(); });
return true;
}
}
}
return QWidget::eventFilter(obj, event);
}
void TitleBarWidget::updateMaximizeIcon() {

View File

@@ -18,6 +18,7 @@ public:
void setShowIcon(bool show);
void setMenuBarTitleCase(bool titleCase);
bool menuBarTitleCase() const { return m_titleCase; }
void finalizeMenuBar();
void updateMaximizeIcon();
@@ -25,16 +26,20 @@ protected:
void mousePressEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent* event) override;
bool eventFilter(QObject* obj, QEvent* event) override;
private:
QLabel* m_appLabel = nullptr;
QMenuBar* m_menuBar = nullptr;
QHBoxLayout* m_menuBtnLayout = nullptr;
QVector<QToolButton*> m_menuButtons;
QToolButton* m_btnMin = nullptr;
QToolButton* m_btnMax = nullptr;
QToolButton* m_btnClose = nullptr;
Theme m_theme;
bool m_titleCase = false;
bool m_useToolButtons = false;
QToolButton* makeChromeButton(const QString& iconPath);
void toggleMaximize();

View File

@@ -191,23 +191,26 @@ inline FeatureResult countFlagFeatures(uint32_t val,
// ── Pointer feature checker ──
inline FeatureResult countPtrFeatures64(uint64_t val) {
// Hard reject: common sentinel values are never pointers
// Hard reject: common sentinel values
if (val == 0 || val == 0xFFFFFFFFFFFFFFFFULL || val == 0x00000000FFFFFFFFULL)
return {0, 6};
return {0, 5};
int passed = 0, checked = 6;
// Feature 1: canonical 48-bit address (sign-extended from bit 47)
passed += (val <= 0x00007FFFFFFFFFFFULL
|| val >= 0xFFFF800000000000ULL) ? 1 : 0;
// Feature 2: aligned to 8 (heap/vtable allocations)
// Hard reject: non-canonical address — impossible to dereference on x64
// User-mode: 0x0000000000000000 0x00007FFFFFFFFFFF
// Kernel: 0xFFFF800000000000 0xFFFFFFFFFFFFFFFF
if (val > 0x00007FFFFFFFFFFFULL && val < 0xFFFF800000000000ULL)
return {0, 5};
int passed = 0, checked = 5;
// Feature 1: aligned to 8 (heap/vtable allocations)
passed += ((val & 7) == 0) ? 1 : 0;
// Feature 3: above null guard pages (real addresses >= 64KB)
// Feature 2: above null guard pages (real addresses >= 64KB)
passed += (val >= 0x10000) ? 1 : 0;
// Feature 4: has upper 32 bits (real 64-bit address, not a small constant)
// Feature 3: has upper 32 bits (real 64-bit address, not a small constant)
passed += ((val >> 32) != 0) ? 1 : 0;
// Feature 5: above 4GB (in real 64-bit address space, not a 32-bit value)
// Feature 4: above 4GB (in real 64-bit address space)
passed += (val > 0x100000000ULL) ? 1 : 0;
// Feature 6: user-mode address range (not kernel 0xFFFF800000000000+)
// Feature 5: user-mode address range (not kernel)
passed += (val < 0xFFFF800000000000ULL) ? 1 : 0;
return {passed, checked};
}
@@ -289,13 +292,13 @@ struct Candidate {
};
inline void addCandidate(QVector<Candidate>& out, NodeKind k, int score) {
if (score >= 25) out.append({{k}, score});
if (score >= 25) out.push_back(Candidate{{k}, score});
}
inline void addSplitCandidate(QVector<Candidate>& out, NodeKind k, int count, int score) {
if (score >= 25) {
QVector<NodeKind> kinds(count, k);
out.append({std::move(kinds), score});
out.push_back(Candidate{std::move(kinds), score});
}
}
@@ -308,32 +311,43 @@ inline void tryWhole8(const uint8_t* data, const InferHints& h, QVector<Candidat
if (h.ptrSize == 8)
addCandidate(out, NodeKind::Pointer64, featureScore(countPtrFeatures64(u64)));
// Double
// Double — rare in RE work; require strong evidence
{
double d; std::memcpy(&d, data, 8);
uint64_t exp = (u64 >> 52) & 0x7FF;
int passed = 0, checked = 3;
passed += std::isfinite(d) ? 1 : 0;
passed += (exp > 0 || (u64 & 0x7FFFFFFFFFFFFFFFull) == 0) ? 1 : 0;
double ad = std::fabs(d);
passed += (d == 0.0 || (ad >= 1e-6 && ad <= 1e12)) ? 1 : 0;
addCandidate(out, NodeKind::Double, featureScore({passed, checked}));
uint64_t mantissa = u64 & 0x000FFFFFFFFFFFFFull;
// Hard reject: outside plausible range [1e-6, 1e7] (matches float checker)
bool inRange = (d == 0.0 || (ad >= 1e-6 && ad <= 1e7));
// Hard reject: lower 32 zero with non-zero mantissa (two 32-bit fields)
bool splitField = ((u64 & 0xFFFFFFFF) == 0 && mantissa != 0);
if (inRange && !splitField) {
uint64_t exp = (u64 >> 52) & 0x7FF;
int passed = 0, checked = 4;
// Feature 1: finite
passed += std::isfinite(d) ? 1 : 0;
// Feature 2: non-denormal
passed += (exp > 0 || (u64 & 0x7FFFFFFFFFFFFFFFull) == 0) ? 1 : 0;
// Feature 3: has fractional part or is a small special value
double ip; double frac = std::fabs(std::modf(d, &ip));
passed += (frac > 0.001 || ad <= 1.0) ? 1 : 0;
// Feature 4: not a large exact integer (likely reinterpreted binary data)
passed += !(ad > 1000.0 && frac < 0.001) ? 1 : 0;
addCandidate(out, NodeKind::Double, featureScore({passed, checked}));
}
}
// UTF8
addCandidate(out, NodeKind::UTF8, featureScore(countStringFeatures(data, 8)));
// UInt64 / Int64
{
int passed = 0, checked = 4;
// Feature 1: fits in 32 bits (small constant, not an address)
passed += (u64 <= 0xFFFFFFFFull) ? 1 : 0;
// Feature 2: upper 32 bits are zero (confirms it's a small value, not a pointer)
passed += ((u64 >> 32) == 0) ? 1 : 0;
// Feature 3: non-zero
passed += (u64 != 0) ? 1 : 0;
// Feature 4: monotonic or very small (< 0x10000)
passed += (h.monotonic || u64 < 0x10000) ? 1 : 0;
// UInt64 / Int64 — only meaningful when value exceeds 32-bit range
if ((u64 >> 32) != 0) {
int passed = 0, checked = 3;
// Feature 1: non-zero (always true after guard)
passed += 1;
// Feature 2: reasonable magnitude (below kernel range)
passed += (u64 < 0x0000FFFFFFFFFFFFULL) ? 1 : 0;
// Feature 3: monotonic or page-aligned
passed += (h.monotonic || (u64 & 0xFFF) == 0) ? 1 : 0;
addCandidate(out, NodeKind::UInt64, featureScore({passed, checked}));
}
}
@@ -467,7 +481,7 @@ inline QVector<TypeSuggestion> pruneAndRank(QVector<Candidate>& cands, int maxRe
for (const auto& c : deduped) {
int str = strengthFromScore(c.score);
if (str > 0)
result.append({c.kinds, c.score, str});
result.push_back(TypeSuggestion{c.kinds, c.score, str});
}
return result;
}

View File

@@ -1030,7 +1030,7 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
else if (t.category == TypeEntry::CatEnum) enumCount++;
else typeCount++;
if (catAllowed(t))
scored.append({i, sc, std::move(pos)});
scored.push_back(Scored{i, sc, std::move(pos)});
}
std::sort(scored.begin(), scored.end(),
[](const Scored& a, const Scored& b) { return a.score > b.score; });

View File

@@ -108,9 +108,9 @@ inline void buildProjectExplorer(QStandardItemModel* model,
const Node& n = tab.tree->nodes[idx];
if (n.kind != NodeKind::Struct) continue;
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
enums.append({&n, tab.subPtr, tab.tree});
enums.push_back(Entry{&n, tab.subPtr, tab.tree});
else
types.append({&n, tab.subPtr, tab.tree});
types.push_back(Entry{&n, tab.subPtr, tab.tree});
}
}
@@ -152,7 +152,7 @@ inline void syncProjectExplorer(QStandardItemModel* model,
const Node& n = tab.tree->nodes[idx];
if (n.kind != NodeKind::Struct) continue;
bool ie = n.resolvedClassKeyword() == QStringLiteral("enum");
desired.append({n.id, &n, tab.subPtr, tab.tree, ie});
desired.push_back(Entry{n.id, &n, tab.subPtr, tab.tree, ie});
}
}

View File

@@ -202,7 +202,7 @@ void BenchProject::benchBuildWorkspaceModel()
// Build TabInfo array
QVector<TabInfo> tabs;
for (const auto& t : trees)
tabs.append({ &t, QStringLiteral("test"), nullptr });
tabs.push_back(TabInfo{ &t, QStringLiteral("test"), nullptr });
QStandardItemModel model;
const int ITERS = 20;
@@ -244,7 +244,7 @@ void BenchProject::benchWorkspaceSearch()
QVector<TabInfo> tabs;
for (const auto& t : trees)
tabs.append({ &t, QStringLiteral("test"), nullptr });
tabs.push_back(TabInfo{ &t, QStringLiteral("test"), nullptr });
QStandardItemModel model;
buildProjectExplorer(&model, tabs);

222
tests/grab_tabs.cpp Normal file
View File

@@ -0,0 +1,222 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QMainWindow>
#include <QDockWidget>
#include <QTabBar>
#include <QTextEdit>
#include <QPixmap>
#include <QToolButton>
#include <QHBoxLayout>
#include <QProxyStyle>
#include <QStyleOptionTab>
#include <QSettings>
#include <QPainter>
#include "../src/themes/thememanager.h"
// Minimal replica of the real app's MenuBarStyle for dock tab painting
class TestTabStyle : public QProxyStyle {
public:
using QProxyStyle::QProxyStyle;
QSize sizeFromContents(ContentsType type, const QStyleOption* opt,
const QSize& sz, const QWidget* w) const override {
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
if (type == CT_TabBarTab) {
if (auto* tabBar = qobject_cast<const QTabBar*>(w)) {
if (tabBar->parent() && qobject_cast<const QMainWindow*>(tabBar->parent()))
s.setHeight(28);
}
}
return s;
}
void drawControl(ControlElement element, const QStyleOption* opt,
QPainter* p, const QWidget* w) const override {
// Tab shape — background, accent line, borders
if (element == CE_TabBarTabShape) {
if (auto* tab = qstyleoption_cast<const QStyleOptionTab*>(opt)) {
auto* tabBar = qobject_cast<const QTabBar*>(w);
if (tabBar && tabBar->parent() && qobject_cast<QMainWindow*>(tabBar->parent())) {
bool selected = tab->state & State_Selected;
bool hovered = tab->state & State_MouseOver;
QColor bg = tab->palette.color(QPalette::Window);
if (hovered && !selected)
bg = tab->palette.color(QPalette::Mid);
p->fillRect(tab->rect, bg);
if (selected)
p->fillRect(QRect(tab->rect.left(), tab->rect.top(),
tab->rect.width(), 2),
tab->palette.color(QPalette::Link));
p->setPen(tab->palette.color(QPalette::Dark));
p->drawLine(tab->rect.bottomLeft(), tab->rect.bottomRight());
return;
}
}
}
// Tab label — middle-elide long names, editor font
if (element == CE_TabBarTabLabel) {
if (auto* tab = qstyleoption_cast<const QStyleOptionTab*>(opt)) {
auto* tabBar = qobject_cast<const QTabBar*>(w);
if (tabBar && tabBar->parent() && qobject_cast<QMainWindow*>(tabBar->parent())) {
int tabIdx = -1;
for (int i = 0; i < tabBar->count(); ++i) {
if (tabBar->tabRect(i).contains(tab->rect.center())) { tabIdx = i; break; }
}
int btnWidth = 0;
if (tabIdx >= 0) {
auto* btn = tabBar->tabButton(tabIdx, QTabBar::RightSide);
if (btn) btnWidth = btn->sizeHint().width() + 4;
}
QRect textRect = tab->rect.adjusted(8, 0, -(8 + btnWidth), 0);
QFont f("JetBrains Mono", 10);
f.setFixedPitch(true);
p->setFont(f);
QFontMetrics fm(f);
QString text = (tabIdx >= 0) ? tabBar->tabText(tabIdx) : tab->text;
int maxW = textRect.width();
if (fm.horizontalAdvance(text) > maxW) {
int ellW = fm.horizontalAdvance(QStringLiteral("\u2026"));
int avail = maxW - ellW;
if (avail > 0) {
int half = avail / 2;
QString left, right;
for (int i = 0; i < text.size(); ++i)
if (fm.horizontalAdvance(text.left(i+1)) > half) { left = text.left(i); break; }
if (left.isEmpty()) left = text.left(1);
for (int i = text.size()-1; i >= 0; --i)
if (fm.horizontalAdvance(text.mid(i)) > half) { right = text.mid(i+1); break; }
if (right.isEmpty()) right = text.right(1);
text = left + QStringLiteral("\u2026") + right;
} else {
text = QStringLiteral("\u2026");
}
}
bool selected = tab->state & QStyle::State_Selected;
p->setPen(selected ? tab->palette.color(QPalette::Text)
: tab->palette.color(QPalette::WindowText));
p->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, text);
return;
}
}
}
QProxyStyle::drawControl(element, opt, p, w);
}
};
class TabBtns : public QWidget {
public:
explicit TabBtns(const QColor& hover, QWidget* parent = nullptr) : QWidget(parent) {
auto* hl = new QHBoxLayout(this);
hl->setContentsMargins(2, 0, 0, 0);
hl->setSpacing(0);
QString style = QStringLiteral(
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
"QToolButton:hover { background: %1; }").arg(hover.name());
auto* pin = new QToolButton(this);
pin->setFixedSize(16, 16);
pin->setAutoRaise(true);
pin->setIcon(QIcon(":/vsicons/pin.svg"));
pin->setIconSize(QSize(12, 12));
pin->setStyleSheet(style);
hl->addWidget(pin);
auto* close = new QToolButton(this);
close->setFixedSize(16, 16);
close->setAutoRaise(true);
close->setIcon(QIcon(":/vsicons/close.svg"));
close->setIconSize(QSize(12, 12));
close->setStyleSheet(style);
hl->addWidget(close);
}
};
class GrabTabs : public QObject {
Q_OBJECT
private slots:
void grab() {
const auto& t = rcx::ThemeManager::instance().current();
// Install custom style (no stylesheet — all painting via style)
QApplication::setStyle(new TestTabStyle("Fusion"));
// Apply dark palette globally
QPalette pal;
pal.setColor(QPalette::Window, t.background);
pal.setColor(QPalette::WindowText, t.textDim);
pal.setColor(QPalette::Base, t.background);
pal.setColor(QPalette::Text, t.text);
pal.setColor(QPalette::Mid, t.hover);
pal.setColor(QPalette::Dark, t.border);
pal.setColor(QPalette::Link, t.indHoverSpan);
QApplication::setPalette(pal);
auto* win = new QMainWindow;
win->resize(700, 500);
win->setDockNestingEnabled(true);
win->setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North);
auto* central = new QWidget(win);
central->setMaximumSize(0, 0);
central->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
win->setCentralWidget(central);
win->setStyleSheet(QStringLiteral(
"QMainWindow::separator { width: 0px; height: 0px; background: transparent; }"));
QStringList names = {
"shader_color_helper.hpp",
"shader_crypt.cpp",
"EPROCESS (class)",
"very_long_struct_name_that_should_elide.h"
};
QVector<QDockWidget*> docks;
for (const auto& name : names) {
auto* dock = new QDockWidget(name, win);
dock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
auto* emptyTitle = new QWidget(dock);
emptyTitle->setFixedHeight(0);
dock->setTitleBarWidget(emptyTitle);
dock->setWidget(new QTextEdit(dock));
if (!docks.isEmpty())
win->tabifyDockWidget(docks.last(), dock);
else
win->addDockWidget(Qt::TopDockWidgetArea, dock);
docks.append(dock);
}
// Select first tab
docks.first()->raise();
win->show();
QVERIFY(QTest::qWaitForWindowExposed(win));
QApplication::processEvents();
// No stylesheet on dock tab bars — painting handled by TestTabStyle
for (auto* tabBar : win->findChildren<QTabBar*>()) {
if (tabBar->parent() != win) continue;
tabBar->setStyleSheet(QString());
tabBar->setElideMode(Qt::ElideNone);
tabBar->setExpanding(false);
QPalette tp = tabBar->palette();
tp.setColor(QPalette::WindowText, t.textDim);
tp.setColor(QPalette::Text, t.text);
tp.setColor(QPalette::Window, t.background);
tp.setColor(QPalette::Mid, t.hover);
tp.setColor(QPalette::Dark, t.border);
tp.setColor(QPalette::Link, t.indHoverSpan);
tabBar->setPalette(tp);
for (int i = 0; i < tabBar->count(); ++i)
tabBar->setTabButton(i, QTabBar::RightSide, new TabBtns(t.hover, tabBar));
}
QApplication::processEvents();
QApplication::processEvents();
QPixmap shot = win->grab(QRect(0, 0, win->width(), 50));
shot.save(QStringLiteral("tab_screenshot.png"));
qDebug() << "Saved" << shot.size();
delete win;
}
};
QTEST_MAIN(GrabTabs)
#include "grab_tabs.moc"

View File

@@ -382,6 +382,30 @@ private slots:
QCOMPARE(r.value, 0x140000000ULL);
}
// -- Bare module.dll identifier --
void bareModuleDll() {
AddressParserCallbacks cbs;
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "client.dll");
return *ok ? 0x7FF600000000ULL : 0;
};
auto r = AddressParser::evaluate("client.dll + 0xFF", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0x7FF6000000FFULL);
}
void bareModuleExe() {
AddressParserCallbacks cbs;
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "cs2.exe");
return *ok ? 0x140000000ULL : 0;
};
auto r = AddressParser::evaluate("cs2.exe + 0xDE", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0x1400000DEULL);
}
// -- Validate with new syntax --
void validateIdentifier() {

View File

@@ -230,7 +230,7 @@ private slots:
// Only include the pointer-expanded ones (near vtable at 0x100)
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
int nodeIdx = lm.nodeIdx;
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
funcPtrs.push_back(FuncInfo{i, lm.offsetAddr, lm.nodeKind,
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
}
}

View File

@@ -0,0 +1,397 @@
#include <QTest>
#include <QSignalSpy>
#include <QByteArray>
#include <cstring>
#include "providers/provider.h"
#include "scanner.h"
#include "../plugins/KernelMemory/KernelMemoryPlugin.h"
#ifdef _WIN32
#include <windows.h>
#include <tlhelp32.h>
#endif
using namespace rcx;
class TestKernelProvider : public QObject {
Q_OBJECT
private:
bool m_driverAvailable = false;
KernelMemoryPlugin* m_plugin = nullptr;
std::unique_ptr<Provider> m_provider;
uint32_t m_selfPid = 0;
private slots:
// ── Setup: try to load driver, skip tests if unavailable ──
void initTestCase()
{
m_plugin = new KernelMemoryPlugin();
#ifdef _WIN32
m_selfPid = GetCurrentProcessId();
// Try to open driver directly to see if it's available
HANDLE h = CreateFileA(RCX_DRV_USERMODE_PATH,
GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (h != INVALID_HANDLE_VALUE) {
CloseHandle(h);
m_driverAvailable = true;
} else {
// Try loading via plugin
QString errorMsg;
QString target = QStringLiteral("km:%1:self").arg(m_selfPid);
m_provider = m_plugin->createProvider(target, &errorMsg);
if (m_provider && m_provider->isValid()) {
m_driverAvailable = true;
} else {
qWarning("Kernel driver not available: %s", qPrintable(errorMsg));
qWarning("Tests requiring the driver will be skipped.");
}
}
if (m_driverAvailable && !m_provider) {
QString target = QStringLiteral("km:%1:self").arg(m_selfPid);
m_provider = m_plugin->createProvider(target, nullptr);
}
#endif
}
void cleanupTestCase()
{
m_provider.reset();
delete m_plugin;
m_plugin = nullptr;
}
// ── 1. Plugin metadata (no driver needed) ──
void plugin_name()
{
QCOMPARE(QString::fromStdString(m_plugin->Name()), QStringLiteral("Kernel Memory"));
}
void plugin_loadType()
{
QCOMPARE(m_plugin->LoadType(), IPlugin::k_ELoadTypeManual);
}
void plugin_canHandle()
{
QVERIFY(m_plugin->canHandle(QStringLiteral("km:1234:test.exe")));
QVERIFY(m_plugin->canHandle(QStringLiteral("phys:0")));
QVERIFY(m_plugin->canHandle(QStringLiteral("msr:")));
QVERIFY(!m_plugin->canHandle(QStringLiteral("1234:test.exe")));
QVERIFY(!m_plugin->canHandle(QStringLiteral("file:test.bin")));
}
void provider_noDriver_invalid()
{
// Creating provider with invalid target should fail gracefully
QString err;
auto prov = m_plugin->createProvider(QStringLiteral("km:0:invalid"), &err);
// Either nullptr or invalid -- both are acceptable
if (prov) QVERIFY(!prov->isValid() || prov->size() == 0);
}
// ── 2. KUSER_SHARED_DATA validation (at 0x7FFE0000) ──
void kusd_ntMajorVersion()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
QVERIFY(m_provider);
// KUSER_SHARED_DATA.NtMajorVersion at offset 0x26C
uint32_t major = m_provider->readU32(0x7FFE0000 + 0x26C);
QCOMPARE(major, (uint32_t)10); // Windows 10/11
}
void kusd_ntMinorVersion()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
uint32_t minor = m_provider->readU32(0x7FFE0000 + 0x270);
QCOMPARE(minor, (uint32_t)0); // Windows 10+ has minor = 0
}
void kusd_ntBuildNumber()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
// Cross-validate with RtlGetVersion
typedef NTSTATUS(NTAPI* RtlGetVersion_t)(PRTL_OSVERSIONINFOW);
auto pRtlGetVersion = (RtlGetVersion_t)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "RtlGetVersion");
QVERIFY(pRtlGetVersion);
RTL_OSVERSIONINFOW osvi{};
osvi.dwOSVersionInfoSize = sizeof(osvi);
QCOMPARE(pRtlGetVersion(&osvi), (NTSTATUS)0);
uint32_t buildFromDriver = m_provider->readU32(0x7FFE0000 + 0x260);
QCOMPARE(buildFromDriver, (uint32_t)osvi.dwBuildNumber);
#endif
}
void kusd_systemTime_nonZero()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
uint64_t sysTime = m_provider->readU64(0x7FFE0000 + 0x14);
QVERIFY(sysTime != 0);
}
void kusd_tickCount_increasing()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
// TickCountMultiplier at 0x4, TickCount at 0x320
uint64_t tick1 = m_provider->readU64(0x7FFE0000 + 0x320);
QTest::qWait(120);
uint64_t tick2 = m_provider->readU64(0x7FFE0000 + 0x320);
QVERIFY2(tick2 > tick1,
qPrintable(QStringLiteral("tick1=%1 tick2=%2").arg(tick1).arg(tick2)));
}
void kusd_crossValidate_readProcessMemory()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
// Read same KUSD page through driver and ReadProcessMemory
QByteArray driverBuf(256, 0);
m_provider->read(0x7FFE0000, driverBuf.data(), 256);
QByteArray rpmBuf(256, 0);
SIZE_T bytesRead = 0;
HANDLE self = GetCurrentProcess();
ReadProcessMemory(self, (LPCVOID)0x7FFE0000, rpmBuf.data(), 256, &bytesRead);
// NtMajorVersion (offset 0x26C relative = not in first 256 bytes, so compare what we have)
// Compare first 256 bytes -- should be identical
QCOMPARE(driverBuf, rpmBuf);
#endif
}
// ── 3. Self-read integration ──
void selfRead_mzHeader()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
uint64_t selfBase = (uint64_t)GetModuleHandleA(nullptr);
QVERIFY(selfBase != 0);
uint8_t mz[2] = {};
m_provider->read(selfBase, mz, 2);
QCOMPARE(mz[0], (uint8_t)'M');
QCOMPARE(mz[1], (uint8_t)'Z');
#endif
}
void selfRead_peSignature()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
uint64_t selfBase = (uint64_t)GetModuleHandleA(nullptr);
// PE offset at +0x3C
uint32_t peOffset = m_provider->readU32(selfBase + 0x3C);
QVERIFY(peOffset > 0 && peOffset < 0x1000);
// PE signature = "PE\0\0" = 0x00004550
uint32_t peSig = m_provider->readU32(selfBase + peOffset);
QCOMPARE(peSig, (uint32_t)0x00004550);
#endif
}
// ── 4. Scanner integration ──
void scanner_mzSigScan()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
auto shared = std::shared_ptr<Provider>(m_provider.get(), [](Provider*){});
ScanRequest req;
req.pattern = QByteArray("\x4D\x5A", 2);
req.mask = QByteArray("\xFF\xFF", 2);
req.alignment = 1;
req.maxResults = 10;
// Constrain to our own module for speed
uint64_t selfBase = (uint64_t)GetModuleHandleA(nullptr);
req.startAddress = selfBase;
req.endAddress = selfBase + 0x1000;
ScanEngine engine;
QSignalSpy spy(&engine, &ScanEngine::finished);
engine.start(shared, req);
QVERIFY(spy.wait(5000));
auto results = spy.at(0).at(0).value<QVector<ScanResult>>();
QVERIFY(results.size() >= 1);
QCOMPARE(results[0].address, selfBase);
#endif
}
// ── 5. Region enumeration ──
void regions_selfProcess()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
auto regions = m_provider->enumerateRegions();
QVERIFY(regions.size() > 0);
// Should have at least one executable region (our code)
bool hasExec = false;
for (const auto& r : regions) {
if (r.executable) { hasExec = true; break; }
}
QVERIFY(hasExec);
}
// ── 6. PEB / modules ──
void peb_nonZero()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
QVERIFY(m_provider->peb() != 0);
}
void symbol_selfModule()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
uint64_t selfBase = (uint64_t)GetModuleHandleA(nullptr);
QString sym = m_provider->getSymbol(selfBase + 0x100);
QVERIFY(!sym.isEmpty());
QVERIFY(sym.contains(QStringLiteral("+0x")));
#endif
}
// ── 7. CR3 / address translation ──
void cr3_nonZero()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
auto* kprov = dynamic_cast<KernelProcessProvider*>(m_provider.get());
QVERIFY(kprov);
uint64_t cr3 = kprov->getCr3();
QVERIFY2(cr3 != 0, "CR3 should be non-zero for a running process");
// CR3 should be page-aligned (low 12 bits cleared)
QCOMPARE(cr3 & 0xFFF, (uint64_t)0);
}
void vtop_kusd()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
auto* kprov = dynamic_cast<KernelProcessProvider*>(m_provider.get());
QVERIFY(kprov);
// KUSER_SHARED_DATA is at VA 0x7FFE0000 in every process
auto result = kprov->translateAddress(0x7FFE0000);
QVERIFY2(result.valid, "KUSER_SHARED_DATA should be mapped");
QVERIFY(result.physical != 0);
// PML4E and PDPTE should be present
QVERIFY(result.pml4e & 1); // Present bit
QVERIFY(result.pdpte & 1); // Present bit
}
void vtop_selfModule()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
auto* kprov = dynamic_cast<KernelProcessProvider*>(m_provider.get());
QVERIFY(kprov);
uint64_t selfBase = (uint64_t)GetModuleHandleA(nullptr);
auto result = kprov->translateAddress(selfBase);
QVERIFY2(result.valid, "Own module base should be mapped");
QVERIFY(result.physical != 0);
// Cross-validate: read MZ header via physical address
// Read the first 2 bytes at the physical address using physical provider
auto physEntries = kprov->readPageTable(kprov->getCr3(), 0, 16);
QVERIFY(physEntries.size() > 0); // Should get at least some PML4 entries
#endif
}
void vtop_unmapped()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
auto* kprov = dynamic_cast<KernelProcessProvider*>(m_provider.get());
QVERIFY(kprov);
// Address 0 should not be mapped in user mode
auto result = kprov->translateAddress(0);
QVERIFY2(!result.valid, "Address 0 should not be mapped");
}
void readPageTable_cr3()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
auto* kprov = dynamic_cast<KernelProcessProvider*>(m_provider.get());
QVERIFY(kprov);
uint64_t cr3 = kprov->getCr3();
QVERIFY(cr3 != 0);
// Read the full PML4 table (512 entries)
auto entries = kprov->readPageTable(cr3, 0, 512);
QCOMPARE(entries.size(), 512);
// At least some entries should be present (kernel maps upper half)
int presentCount = 0;
for (const auto& e : entries) {
if (e & 1) presentCount++;
}
QVERIFY2(presentCount > 0,
qPrintable(QStringLiteral("Expected present PML4 entries, got 0")));
}
// ── 8. Ping ──
void ping_version()
{
if (!m_driverAvailable) QSKIP("Driver not loaded");
#ifdef _WIN32
HANDLE h = CreateFileA(RCX_DRV_USERMODE_PATH,
GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (h == INVALID_HANDLE_VALUE) QSKIP("Cannot open driver handle");
RcxDrvPingResponse ping{};
DWORD br = 0;
BOOL ok = DeviceIoControl(h, IOCTL_RCX_PING, nullptr, 0,
&ping, sizeof(ping), &br, nullptr);
CloseHandle(h);
QVERIFY(ok);
QCOMPARE(ping.version, (uint32_t)RCX_DRV_VERSION);
#endif
}
};
QTEST_MAIN(TestKernelProvider)
#include "test_kernel_provider.moc"

View File

@@ -26,7 +26,7 @@ public:
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});
m_clients.push_back(Client{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++)

143
tests/test_project_dock.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QMainWindow>
#include <QDockWidget>
#include <QTabWidget>
#include <QTextEdit>
// Replicates the real app layout: QTabWidget central widget, project dock in LeftDockWidgetArea.
class TestProjectDock : public QObject {
Q_OBJECT
private:
struct AppLayout {
QMainWindow* win;
QTabWidget* tabs;
QDockWidget* project;
};
AppLayout buildApp() {
auto* win = new QMainWindow;
win->resize(1280, 800);
// QTabWidget as central widget — same as real app
auto* tabs = new QTabWidget(win);
tabs->setTabsClosable(true);
tabs->setMovable(true);
tabs->setDocumentMode(true);
tabs->addTab(new QTextEdit(tabs), "Untitled");
win->setCentralWidget(tabs);
// Project dock — same as real app
auto* project = new QDockWidget("Project", win);
project->setObjectName("WorkspaceDock");
project->setAllowedAreas(Qt::AllDockWidgetAreas);
project->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
project->setWidget(new QTextEdit(project));
win->addDockWidget(Qt::LeftDockWidgetArea, project);
project->hide();
return {win, tabs, project};
}
void showProject(AppLayout& a) {
if (a.project->isHidden() && !a.project->isFloating()) {
a.win->addDockWidget(Qt::LeftDockWidgetArea, a.project);
a.project->show();
a.win->resizeDocks({a.project}, {qMax(200, a.win->width() / 5)}, Qt::Horizontal);
} else {
a.project->show();
}
}
private slots:
void dockStartsLeft();
void dockWidthIsReasonable();
void dockStaysLeftAfterHideShow();
void dockRespectsDragAfterShow();
};
void TestProjectDock::dockStartsLeft()
{
auto app = buildApp();
app.win->show();
QTest::qWaitForWindowExposed(app.win);
showProject(app);
QApplication::processEvents();
// Project should be to the left of the central tab widget
QVERIFY2(app.project->x() < app.tabs->x(),
qPrintable(QString("Project x=%1, Tabs x=%2")
.arg(app.project->x()).arg(app.tabs->x())));
delete app.win;
}
void TestProjectDock::dockWidthIsReasonable()
{
auto app = buildApp();
app.win->show();
QTest::qWaitForWindowExposed(app.win);
showProject(app);
QApplication::processEvents();
int dockWidth = app.project->width();
int winWidth = app.win->width();
double ratio = (double)dockWidth / winWidth;
qDebug() << "Dock width:" << dockWidth << "Window width:" << winWidth
<< "Ratio:" << QString::number(ratio * 100, 'f', 1) + "%";
QVERIFY2(ratio < 0.40,
qPrintable(QString("Dock too wide: %1% of window").arg(ratio * 100, 0, 'f', 1)));
QVERIFY2(ratio > 0.10,
qPrintable(QString("Dock too narrow: %1% of window").arg(ratio * 100, 0, 'f', 1)));
delete app.win;
}
void TestProjectDock::dockStaysLeftAfterHideShow()
{
auto app = buildApp();
app.win->show();
QTest::qWaitForWindowExposed(app.win);
showProject(app);
QApplication::processEvents();
QVERIFY(app.project->x() < app.tabs->x());
app.project->hide();
QApplication::processEvents();
showProject(app);
QApplication::processEvents();
QVERIFY2(app.project->x() < app.tabs->x(),
qPrintable(QString("After re-show: Project x=%1, Tabs x=%2")
.arg(app.project->x()).arg(app.tabs->x())));
delete app.win;
}
void TestProjectDock::dockRespectsDragAfterShow()
{
auto app = buildApp();
app.win->show();
QTest::qWaitForWindowExposed(app.win);
showProject(app);
QApplication::processEvents();
QVERIFY(app.project->x() < app.tabs->x());
// Simulate user dragging to right
app.win->addDockWidget(Qt::RightDockWidgetArea, app.project);
QApplication::processEvents();
QCOMPARE(app.win->dockWidgetArea(app.project), Qt::RightDockWidgetArea);
// Dock is visible — showProject should NOT force it back to left
showProject(app);
QApplication::processEvents();
QCOMPARE(app.win->dockWidgetArea(app.project), Qt::RightDockWidgetArea);
delete app.win;
}
QTEST_MAIN(TestProjectDock)
#include "test_project_dock.moc"

View File

@@ -0,0 +1,307 @@
#include <QtTest/QTest>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <algorithm>
#include <random>
#include "core.h"
#include "imports/import_source.h"
#include "generator.h"
using namespace rcx;
class TestRoundtripWinSdk : public QObject {
Q_OBJECT
private:
NodeTree fullTree;
QVector<int> rootIndices;
private slots:
void initTestCase();
void importCount();
void pebOffsets();
void roundTrip30();
void generateRcx();
};
void TestRoundtripWinSdk::initTestCase()
{
QString path = QStringLiteral(WINSDK_HEADER_PATH);
QFile file(path);
QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text),
qPrintable("Cannot open " + path));
QString source = QString::fromUtf8(file.readAll());
QVERIFY(!source.isEmpty());
QString err;
fullTree = importFromSource(source, &err, 8);
for (int i = 0; i < fullTree.nodes.size(); i++) {
const auto& n = fullTree.nodes[i];
if (n.parentId == 0 && n.kind == NodeKind::Struct)
rootIndices.append(i);
}
qDebug() << "Imported" << fullTree.nodes.size() << "total nodes,"
<< rootIndices.size() << "root structs";
}
void TestRoundtripWinSdk::importCount()
{
QVERIFY2(rootIndices.size() >= 3000,
qPrintable(QString("Expected >= 3000 roots, got %1").arg(rootIndices.size())));
}
void TestRoundtripWinSdk::pebOffsets()
{
// Verify _PEB field offsets match WinDbg dt ntdll!_PEB
int pebIdx = -1;
for (int i = 0; i < fullTree.nodes.size(); i++) {
if (fullTree.nodes[i].parentId == 0 &&
fullTree.nodes[i].structTypeName == QStringLiteral("_PEB")) {
pebIdx = i;
break;
}
}
QVERIFY2(pebIdx >= 0, "Could not find _PEB root struct");
uint64_t pebId = fullTree.nodes[pebIdx].id;
// Collect direct children with offsets and sizes
struct ChildInfo { QString name; int offset; int size; NodeKind kind; };
QVector<ChildInfo> children;
for (int i = 0; i < fullTree.nodes.size(); i++) {
if (fullTree.nodes[i].parentId == pebId) {
int sz = sizeForKind(fullTree.nodes[i].kind);
if (sz == 0) sz = fullTree.structSpan(fullTree.nodes[i].id);
if (sz == 0) sz = 1;
children.push_back(ChildInfo{fullTree.nodes[i].name, fullTree.nodes[i].offset, sz, fullTree.nodes[i].kind});
}
}
// Sort by offset
std::sort(children.begin(), children.end(),
[](const ChildInfo& a, const ChildInfo& b) { return a.offset < b.offset; });
// Dump all children for diagnostics
for (const auto& c : children) {
qDebug() << " " << Qt::hex << c.offset << c.name
<< "kind=" << kindToString(c.kind) << "size=" << c.size;
}
// Check for overlaps
int overlapCount = 0;
for (int i = 1; i < children.size(); i++) {
int prevEnd = children[i-1].offset + children[i-1].size;
if (children[i].offset < prevEnd && children[i-1].kind != NodeKind::Struct) {
// Only flag overlaps where previous field has a known size (not struct references)
overlapCount++;
if (overlapCount <= 10)
qDebug() << " OVERLAP:" << children[i].name << "at" << Qt::hex << children[i].offset
<< "overlaps" << children[i-1].name << "(ends at" << Qt::hex << prevEnd << ")";
}
}
// Build name→offset map for field checks
QHash<QString, int> offsets;
QHash<QString, NodeKind> kinds;
for (const auto& c : children) {
offsets[c.name] = c.offset;
kinds[c.name] = c.kind;
}
int failCount = 0;
auto checkField = [&](const QString& name, int expected, bool mustBePointer = false) {
if (!offsets.contains(name)) {
qDebug() << " MISSING:" << name;
failCount++;
return;
}
if (offsets[name] != expected) {
qDebug() << " OFFSET MISMATCH:" << name << "got" << Qt::hex << offsets[name]
<< "expected" << Qt::hex << expected;
failCount++;
return;
}
if (mustBePointer) {
NodeKind k = kinds[name];
if (k != NodeKind::Pointer64 && k != NodeKind::Pointer32) {
qDebug() << " NOT POINTER:" << name << "kind=" << kindToString(k);
failCount++;
}
}
};
// Expected offsets computed from the source header layout (Vergilius-style)
// Note: This header has union ALIGN(8) { KernelCallbackTable; UserSharedInfoPtr; }
// after CrossProcessFlags, which shifts fields +0xC compared to some WinDbg versions.
checkField(QStringLiteral("InheritedAddressSpace"), 0x000);
checkField(QStringLiteral("ReadImageFileExecOptions"), 0x001);
checkField(QStringLiteral("BeingDebugged"), 0x002);
checkField(QStringLiteral("Mutant"), 0x008, true);
checkField(QStringLiteral("ImageBaseAddress"), 0x010, true);
checkField(QStringLiteral("Ldr"), 0x018, true);
checkField(QStringLiteral("ProcessParameters"), 0x020, true);
checkField(QStringLiteral("SubSystemData"), 0x028, true);
checkField(QStringLiteral("ProcessHeap"), 0x030, true);
checkField(QStringLiteral("FastPebLock"), 0x038, true);
checkField(QStringLiteral("AtlThunkSListPtr"), 0x040, true);
checkField(QStringLiteral("IFEOKey"), 0x048, true);
checkField(QStringLiteral("SystemReserved"), 0x060);
checkField(QStringLiteral("AtlThunkSListPtr32"), 0x064);
checkField(QStringLiteral("ApiSetMap"), 0x068, true);
checkField(QStringLiteral("TlsExpansionCounter"), 0x070);
checkField(QStringLiteral("TlsBitmap"), 0x078, true);
checkField(QStringLiteral("TlsBitmapBits"), 0x080);
checkField(QStringLiteral("ReadOnlySharedMemoryBase"), 0x088, true);
checkField(QStringLiteral("SharedData"), 0x090, true);
checkField(QStringLiteral("ReadOnlyStaticServerData"), 0x098, true);
checkField(QStringLiteral("AnsiCodePageData"), 0x0A0, true);
checkField(QStringLiteral("OemCodePageData"), 0x0A8, true);
checkField(QStringLiteral("UnicodeCaseTableData"), 0x0B0, true);
checkField(QStringLiteral("NumberOfProcessors"), 0x0B8);
checkField(QStringLiteral("NtGlobalFlag"), 0x0BC);
checkField(QStringLiteral("HeapSegmentReserve"), 0x0C8);
checkField(QStringLiteral("NumberOfHeaps"), 0x0E8);
checkField(QStringLiteral("MaximumNumberOfHeaps"), 0x0EC);
checkField(QStringLiteral("ProcessHeaps"), 0x0F0, true);
checkField(QStringLiteral("OSMajorVersion"), 0x118);
checkField(QStringLiteral("OSMinorVersion"), 0x11C);
checkField(QStringLiteral("OSBuildNumber"), 0x120);
checkField(QStringLiteral("SessionId"), 0x2C0);
checkField(QStringLiteral("CsrServerReadOnlySharedMemoryBase"), 0x380);
checkField(QStringLiteral("TppWorkerpListLock"), 0x388, true);
checkField(QStringLiteral("WaitOnAddressHashTable"), 0x3A0);
checkField(QStringLiteral("TelemetryCoverageHeader"), 0x7A0, true);
checkField(QStringLiteral("CloudFileFlags"), 0x7A8);
checkField(QStringLiteral("CloudFileDiagFlags"), 0x7AC);
checkField(QStringLiteral("PlaceholderCompatibilityMode"), 0x7B0);
checkField(QStringLiteral("LeapSecondData"), 0x7B8, true);
checkField(QStringLiteral("NtGlobalFlag2"), 0x7C4);
QVERIFY2(failCount == 0,
qPrintable(QString("%1 PEB field(s) have wrong offsets or are missing").arg(failCount)));
}
void TestRoundtripWinSdk::roundTrip30()
{
const int kRequired = 30;
// Deterministic shuffle
QVector<int> shuffled = rootIndices;
std::mt19937 rng(42);
std::shuffle(shuffled.begin(), shuffled.end(), rng);
int passCount = 0;
int failCount = 0;
int skipCount = 0;
for (int ri : shuffled) {
uint64_t rootId = fullTree.nodes[ri].id;
QString structName = fullTree.nodes[ri].structTypeName;
// Pass 1: export from full tree
QString cpp1 = renderCpp(fullTree, rootId, nullptr, true);
if (cpp1.isEmpty()) {
skipCount++;
continue;
}
// Pass 2: re-import
QString err;
NodeTree tree2 = importFromSource(cpp1, &err);
if (tree2.nodes.isEmpty()) {
skipCount++;
continue;
}
// Find the root in re-imported tree
int rootIdx2 = -1;
for (int i = 0; i < tree2.nodes.size(); i++) {
if (tree2.nodes[i].parentId == 0 && tree2.nodes[i].kind == NodeKind::Struct) {
if (tree2.nodes[i].structTypeName == structName) {
rootIdx2 = i;
break;
}
}
}
if (rootIdx2 < 0) {
// Take first root
for (int i = 0; i < tree2.nodes.size(); i++) {
if (tree2.nodes[i].parentId == 0 && tree2.nodes[i].kind == NodeKind::Struct) {
rootIdx2 = i;
break;
}
}
}
if (rootIdx2 < 0) {
skipCount++;
continue;
}
// Pass 3: re-export
QString cpp2 = renderCpp(tree2, tree2.nodes[rootIdx2].id, nullptr, true);
if (cpp1 == cpp2) {
passCount++;
if (passCount <= kRequired)
qDebug() << " PASS" << passCount << structName;
} else {
failCount++;
if (failCount <= 5) {
// Log first few failures for diagnostics
QStringList lines1 = cpp1.split('\n');
QStringList lines2 = cpp2.split('\n');
int diffLine = -1;
for (int i = 0; i < qMin(lines1.size(), lines2.size()); i++) {
if (lines1[i] != lines2[i]) { diffLine = i; break; }
}
if (diffLine >= 0) {
qDebug() << " FAIL" << structName << "first diff at line" << diffLine;
qDebug() << " cpp1:" << lines1[diffLine].left(120);
qDebug() << " cpp2:" << lines2[diffLine].left(120);
} else {
qDebug() << " FAIL" << structName << "line count differs:"
<< lines1.size() << "vs" << lines2.size();
}
}
}
if (passCount >= kRequired && failCount > 5)
break; // found enough passes and logged enough failures
}
qDebug() << "Round-trip results: pass=" << passCount
<< "fail=" << failCount << "skip=" << skipCount;
QVERIFY2(passCount >= kRequired,
qPrintable(QString("Need %1 stable round-trips, got %2")
.arg(kRequired).arg(passCount)));
}
void TestRoundtripWinSdk::generateRcx()
{
// Set all root structs collapsed
for (int ri : rootIndices)
fullTree.nodes[ri].collapsed = true;
fullTree.baseAddress = 0xFFFFF80000000000ULL;
QJsonObject json = fullTree.toJson();
QJsonDocument jdoc(json);
QByteArray data = jdoc.toJson(QJsonDocument::Indented);
QVERIFY2(data.size() > 1000000,
qPrintable(QString("RCX too small: %1 bytes").arg(data.size())));
QString outPath = QStringLiteral(WINSDK_RCX_OUTPUT);
QFile file(outPath);
QVERIFY2(file.open(QIODevice::WriteOnly | QIODevice::Truncate),
qPrintable("Cannot write " + outPath));
file.write(data);
file.close();
qDebug() << "Wrote" << data.size() << "bytes to" << outPath;
}
QTEST_MAIN(TestRoundtripWinSdk)
#include "test_roundtrip_winsdk.moc"

View File

@@ -644,8 +644,8 @@ private slots:
data[16] = 0xAA; // in region 1 (executable)
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, false, "heap"});
regions.append({16, 16, true, false, true, "code"});
regions.push_back(MemoryRegion{0, 16, true, true, false, "heap"});
regions.push_back(MemoryRegion{16, 16, true, false, true, "code"});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
@@ -671,8 +671,8 @@ private slots:
data[16] = 0xBB; // region 1 (not writable)
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, false, "data"});
regions.append({16, 16, true, false, true, "code"});
regions.push_back(MemoryRegion{0, 16, true, true, false, "data"});
regions.push_back(MemoryRegion{16, 16, true, false, true, "code"});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
@@ -698,9 +698,9 @@ private slots:
data[32] = 0xCC; // region 2: +w +x
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, false, "data"});
regions.append({16, 16, true, false, true, "code"});
regions.append({32, 16, true, true, true, "rwx"});
regions.push_back(MemoryRegion{0, 16, true, true, false, "data"});
regions.push_back(MemoryRegion{16, 16, true, false, true, "code"});
regions.push_back(MemoryRegion{32, 16, true, true, true, "rwx"});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
@@ -726,7 +726,7 @@ private slots:
data[0] = 0xDD;
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, true, "Game.exe"});
regions.push_back(MemoryRegion{0, 16, true, true, true, "Game.exe"});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
@@ -943,8 +943,8 @@ private slots:
void provider_customRegions() {
QVector<MemoryRegion> regs;
regs.append({0x1000, 0x2000, true, true, false, "heap"});
regs.append({0x3000, 0x1000, true, false, true, "code"});
regs.push_back(MemoryRegion{0x1000, 0x2000, true, true, false, "heap"});
regs.push_back(MemoryRegion{0x3000, 0x1000, true, false, true, "code"});
RegionProvider p(QByteArray(0x4000, '\0'), regs);
auto result = p.enumerateRegions();
@@ -982,9 +982,9 @@ private slots:
data[36] = 0xEE; // region 2
QVector<MemoryRegion> regions;
regions.append({0, 16, true, true, false, "region0"});
regions.append({16, 16, true, true, false, "region1"});
regions.append({32, 16, true, true, false, "region2"});
regions.push_back(MemoryRegion{0, 16, true, true, false, "region0"});
regions.push_back(MemoryRegion{16, 16, true, true, false, "region1"});
regions.push_back(MemoryRegion{32, 16, true, true, false, "region2"});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
@@ -1215,7 +1215,7 @@ private slots:
data[160] = char(0xCC);
data[210] = char(0xCC);
QVector<MemoryRegion> regions;
regions.append({100, 100, true, false, false, {}});
regions.push_back(MemoryRegion{100, 100, true, false, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1233,7 +1233,7 @@ private slots:
void scan_constrainRegions_noOverlap() {
QByteArray data(32, char(0xEE));
QVector<MemoryRegion> regions;
regions.append({0, 16, true, false, false, {}});
regions.push_back(MemoryRegion{0, 16, true, false, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1256,8 +1256,8 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0, 16, true, true, false, {}});
regions.push_back(MemoryRegion{32, 16, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1279,7 +1279,7 @@ private slots:
data[120] = char(0xAB);
data[160] = char(0xAB);
QVector<MemoryRegion> regions;
regions.append({100, 100, true, true, false, {}});
regions.push_back(MemoryRegion{100, 100, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1300,8 +1300,8 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0x1000, 0x1000, true, false, true, QString("game.exe")});
regions.push_back(MemoryRegion{0x5000, 0x1000, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1345,8 +1345,8 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0, 16, true, true, false, {}});
regions.push_back(MemoryRegion{16, 16, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1368,8 +1368,8 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0x1000, 0x1000, true, false, true, {}});
regions.push_back(MemoryRegion{0x2000, 0x1000, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1394,7 +1394,7 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{10, 10, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1415,7 +1415,7 @@ private slots:
data[5] = char(0xBB);
data[15] = char(0xBB);
QVector<MemoryRegion> regions;
regions.append({0, 32, true, true, false, {}});
regions.push_back(MemoryRegion{0, 32, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1506,7 +1506,7 @@ private slots:
QByteArray data(0x10000, 0);
data[0x8100] = char(0xFF);
QVector<MemoryRegion> regions;
regions.append({0x8000, 0x1000, true, true, false, {}});
regions.push_back(MemoryRegion{0x8000, 0x1000, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1581,7 +1581,7 @@ private slots:
QByteArray data(64, 0);
data[20] = char(0xFE);
QVector<MemoryRegion> regions;
regions.append({0, 64, true, true, false, {}});
regions.push_back(MemoryRegion{0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1602,7 +1602,7 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1624,7 +1624,7 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1643,7 +1643,7 @@ private slots:
// 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, {}});
regions.push_back(MemoryRegion{0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1663,7 +1663,7 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1686,8 +1686,8 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0, 16, true, true, false, {}});
regions.push_back(MemoryRegion{16, 16, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);
@@ -1711,7 +1711,7 @@ private slots:
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, {}});
regions.push_back(MemoryRegion{0, 64, true, true, false, {}});
auto prov = std::make_shared<RegionProvider>(data, regions);
ScanEngine engine;
QSignalSpy finSpy(&engine, &ScanEngine::finished);

View File

@@ -9,32 +9,35 @@
using namespace rcx;
// ─────────────────────────────────────────────────────────────────
// Test suite for the RcxTooltip callout widget
// Test suite for the RcxTooltip arrow callout widget
//
// These tests verify both geometry math AND real-world behavior:
// - Actual pixel rendering (catches WA_TranslucentBackground failures)
// - Leave-event resilience (catches spurious dismiss on tooltip popup)
// - Dismiss correctness (cursor truly leaves trigger zone)
// Validates:
// - Arrow direction auto-detection (above/below based on screen space)
// - Arrow X clamped to stay within rounded corners
// - WA_TranslucentBackground rendering (arrow + body have opaque pixels,
// corners are transparent)
// - Content sizing (title + separator + body)
// ─────────────────────────────────────────────────────────────────
class TestTooltip : public QObject {
Q_OBJECT
private:
QWidget* m_window = nullptr;
QPushButton* m_btnTop = nullptr;
QPushButton* m_btnMid = nullptr;
QPushButton* m_btnLeft = nullptr;
QPushButton* m_btnRight= nullptr;
QWidget* m_window = nullptr;
RcxTooltip* m_tip = nullptr;
void showAndProcess(QWidget* trigger, const QString& text) {
RcxTooltip::instance()->showFor(trigger, text);
// Process events + allow paint to complete
QFont testFont() {
QFont f("JetBrains Mono", 12);
f.setFixedPitch(true);
return f;
}
void showAndProcess(const QPoint& anchor) {
m_tip->showAt(anchor);
QCoreApplication::processEvents();
QTest::qWait(20);
QCoreApplication::processEvents();
}
// Count non-transparent pixels in a QImage region
int countOpaquePixels(const QImage& img, const QRect& region) {
int count = 0;
QRect r = region.intersected(img.rect());
@@ -49,382 +52,180 @@ private slots:
void initTestCase() {
m_window = new QWidget;
m_window->setFixedSize(800, 600);
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
m_window->move(avail.center() - QPoint(400, 300));
m_btnMid = new QPushButton("Middle", m_window);
m_btnMid->setFixedSize(80, 24);
m_btnMid->move(360, 288);
m_btnTop = new QPushButton("Top", m_window);
m_btnTop->setFixedSize(80, 24);
m_btnTop->move(360, 0);
m_btnLeft = new QPushButton("Left", m_window);
m_btnLeft->setFixedSize(80, 24);
m_btnLeft->move(0, 288);
m_btnRight = new QPushButton("Right", m_window);
m_btnRight->setFixedSize(80, 24);
m_btnRight->move(720, 288);
m_window->show();
QVERIFY(QTest::qWaitForWindowExposed(m_window));
m_tip = new RcxTooltip(m_window);
const auto& t = ThemeManager::instance().current();
m_tip->setTheme(t.backgroundAlt, t.border, t.text, t.syntaxNumber, t.border);
}
void cleanupTestCase() {
RcxTooltip::instance()->dismiss();
m_tip->dismiss();
delete m_tip;
delete m_window;
m_window = nullptr;
}
void cleanup() {
RcxTooltip::instance()->dismiss();
m_tip->dismiss();
QCoreApplication::processEvents();
}
// ── Singleton ──
void testSingleton() {
QCOMPARE(RcxTooltip::instance(), RcxTooltip::instance());
}
// ── Basic show/dismiss ──
void testShowAndDismiss() {
auto* tip = RcxTooltip::instance();
QVERIFY(!tip->isVisible());
showAndProcess(m_btnMid, "Hello");
QVERIFY(tip->isVisible());
QCOMPARE(tip->currentText(), QString("Hello"));
QCOMPARE(tip->currentTrigger(), m_btnMid);
tip->dismiss();
QVERIFY(!tip->isVisible());
QVERIFY(tip->currentTrigger() == nullptr);
QVERIFY(!m_tip->isVisible());
m_tip->populate("Title", "Body text", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
QVERIFY(m_tip->isVisible());
m_tip->dismiss();
QVERIFY(!m_tip->isVisible());
}
// ── Empty text / null trigger = dismiss ──
void testEmptyTextDismisses() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Test");
QVERIFY(tip->isVisible());
showAndProcess(m_btnMid, "");
QVERIFY(!tip->isVisible());
// ── Duplicate populate is no-op ──
void testDuplicatePopulateSkipped() {
m_tip->populate("Title", "Body", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
QPoint pos1 = m_tip->pos();
// Same content — populate returns early, position unchanged
m_tip->populate("Title", "Body", testFont());
QCOMPARE(m_tip->pos(), pos1);
}
void testNullTriggerDismisses() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Test");
QVERIFY(tip->isVisible());
showAndProcess(nullptr, "Test");
QVERIFY(!tip->isVisible());
// ── Arrow direction: below when room exists ──
void testArrowUpWhenBelow() {
m_tip->populate("Test", "Below", testFont());
// Anchor in middle of screen — plenty of room below
QPoint anchor = m_window->mapToGlobal(QPoint(400, 300));
showAndProcess(anchor);
QVERIFY(m_tip->isVisible());
// Arrow up (tooltip below anchor): widget top == anchor.y
QCOMPARE(m_tip->y(), anchor.y());
}
// ── Arrow direction ──
void testArrowDownByDefault() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Default placement");
QVERIFY(tip->isVisible());
QVERIFY(tip->arrowPointsDown());
QRect trigGlobal(m_btnMid->mapToGlobal(QPoint(0,0)), m_btnMid->size());
int tipBottom = tip->y() + tip->height();
QVERIFY2(tipBottom <= trigGlobal.top() + RcxTooltip::kGap + 2,
qPrintable(QStringLiteral("tipBottom=%1 trigTop=%2")
.arg(tipBottom).arg(trigGlobal.top())));
}
void testArrowFlipsAtScreenTop() {
// ── Arrow direction: above when no room below ──
void testArrowDownWhenAbove() {
m_tip->populate("Test", "Above", testFont());
// Anchor near bottom of screen
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
QPoint oldPos = m_window->pos();
m_window->move(avail.center().x() - 400, avail.top());
QCoreApplication::processEvents();
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnTop, "Flipped");
QVERIFY(tip->isVisible());
QVERIFY2(!tip->arrowPointsDown(),
"Expected arrow to flip upward when trigger is near screen top");
QRect trigGlobal(m_btnTop->mapToGlobal(QPoint(0,0)), m_btnTop->size());
QVERIFY2(tip->y() >= trigGlobal.bottom(),
qPrintable(QStringLiteral("tipY=%1 trigBottom=%2")
.arg(tip->y()).arg(trigGlobal.bottom())));
m_window->move(oldPos);
QCoreApplication::processEvents();
}
// ── Arrow centering ──
void testArrowCenteredOnTrigger() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Center");
QVERIFY(tip->isVisible());
QRect trigGlobal(m_btnMid->mapToGlobal(QPoint(0,0)), m_btnMid->size());
int trigCenterX = trigGlobal.center().x();
int arrowGlobalX = tip->x() + tip->arrowLocalX();
int delta = qAbs(arrowGlobalX - trigCenterX);
QVERIFY2(delta <= 2,
qPrintable(QStringLiteral("arrowGlobalX=%1 trigCenterX=%2 delta=%3")
.arg(arrowGlobalX).arg(trigCenterX).arg(delta)));
}
// ── Anti-teleport ──
void testNoTeleportSameWidget() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Stable");
QPoint pos1 = tip->pos();
showAndProcess(m_btnMid, "Stable");
QCOMPARE(tip->pos(), pos1);
}
// ── Repositions for different widget ──
void testRepositionsForDifferentWidget() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnLeft, "Left");
QPoint pos1 = tip->pos();
showAndProcess(m_btnRight, "Right");
QVERIFY2(tip->pos() != pos1, "Tooltip should move when trigger widget changes");
QPoint anchor(avail.center().x(), avail.bottom() - 5);
showAndProcess(anchor);
QVERIFY(m_tip->isVisible());
// Arrow down (tooltip above anchor): widget bottom == anchor.y
int tipBottom = m_tip->y() + m_tip->height();
QCOMPARE(tipBottom, anchor.y());
}
// ── Horizontal clamping ──
void testHorizontalClampLeft() {
m_tip->populate("Test", "Wide body text for clamping", testFont());
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
QPoint oldPos = m_window->pos();
m_window->move(avail.left(), avail.center().y() - 300);
QCoreApplication::processEvents();
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnLeft, "Clamped left");
QVERIFY(tip->isVisible());
QVERIFY2(tip->x() >= avail.left(),
qPrintable(QStringLiteral("tipX=%1 screenLeft=%2")
.arg(tip->x()).arg(avail.left())));
m_window->move(oldPos);
QCoreApplication::processEvents();
QPoint anchor(avail.left() + 5, avail.center().y());
showAndProcess(anchor);
QVERIFY(m_tip->x() >= avail.left());
}
void testHorizontalClampRight() {
m_tip->populate("Test", "Wide body text for clamping", testFont());
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
QPoint oldPos = m_window->pos();
m_window->move(avail.right() - m_window->width(), avail.center().y() - 300);
QCoreApplication::processEvents();
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnRight, "Clamped right");
QVERIFY(tip->isVisible());
QVERIFY2(tip->x() + tip->width() <= avail.right() + 2,
qPrintable(QStringLiteral("tipRight=%1 screenRight=%2")
.arg(tip->x() + tip->width()).arg(avail.right())));
m_window->move(oldPos);
QCoreApplication::processEvents();
}
// ── Body rect dimensions ──
void testBodyRectSanity() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Body");
QVERIFY(tip->isVisible());
QRect body = tip->bodyRect();
QVERIFY(body.width() > 0);
QVERIFY(body.height() > 0);
QCOMPARE(tip->height(), body.height() + RcxTooltip::kArrowH);
QPoint anchor(avail.right() - 5, avail.center().y());
showAndProcess(anchor);
QVERIFY(m_tip->x() + m_tip->width() <= avail.right() + 2);
}
// ── Constants ──
void testConstants() {
QCOMPARE(RcxTooltip::kArrowH, 6);
QCOMPARE(RcxTooltip::kArrowHalfW, 6);
QCOMPARE(RcxTooltip::kGap, 2);
QCOMPARE(RcxTooltip::kArrowH, 8);
QCOMPARE(RcxTooltip::kArrowW, 14);
QCOMPARE(RcxTooltip::kRadius, 6);
}
// ── Title-only vs title+body sizing ──
void testTitleOnlySizing() {
m_tip->dismiss();
m_tip->populate("", "Just body", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
int hNoTitle = m_tip->height();
m_tip->dismiss();
m_tip->populate("Title", "Just body", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
int hWithTitle = m_tip->height();
QVERIFY2(hWithTitle > hNoTitle,
"Tooltip with title should be taller than body-only");
}
// ── Multi-line body ──
void testMultilineBody() {
m_tip->dismiss();
m_tip->populate("Title", "Line 1", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
int h1 = m_tip->height();
m_tip->dismiss();
m_tip->populate("Title", "Line 1\nLine 2\nLine 3", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
int h3 = m_tip->height();
QVERIFY2(h3 > h1, "3-line tooltip should be taller than 1-line");
}
// ──────────────────────────────────────────────────────────────
// RENDERING VERIFICATION — catches invisible tooltip bugs
// RENDERING VERIFICATION — WA_TranslucentBackground works
// ──────────────────────────────────────────────────────────────
void testShowForRendersBodyPixels() {
// Show tooltip and grab its rendered pixels.
// Verify that the body area has non-transparent content.
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Render test");
QVERIFY(tip->isVisible());
void testBodyRendersOpaquePixels() {
m_tip->populate("Render", "Test body", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
QVERIFY(m_tip->isVisible());
// Force full opacity so grab gets real pixels
tip->setWindowOpacity(1.0);
QCoreApplication::processEvents();
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
QVERIFY(!img.isNull());
QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
QVERIFY2(!img.isNull(), "grab() returned null image");
QVERIFY2(img.width() > 0 && img.height() > 0, "grab() returned empty image");
// Check center of body for opaque pixels (avoid edges/corners)
QRect center(img.width() / 4, img.height() / 4,
img.width() / 2, img.height() / 2);
int opaque = countOpaquePixels(img, center);
int total = center.width() * center.height();
QVERIFY2(opaque > total / 2,
qPrintable(QStringLiteral("Body center has %1/%2 opaque pixels (<50%%)")
.arg(opaque).arg(total)));
}
// Check body rect area for opaque pixels
QRect body = tip->bodyRect();
// Inset by 2px to avoid anti-aliased border edges
QRect checkRect = body.adjusted(2, 2, -2, -2);
int opaquePixels = countOpaquePixels(img, checkRect);
int totalPixels = checkRect.width() * checkRect.height();
void testCornersAreTransparent() {
m_tip->populate("Corner", "Test", testFont());
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
QVERIFY(m_tip->isVisible());
QVERIFY2(opaquePixels > totalPixels / 2,
qPrintable(QStringLiteral(
"Body area has too few opaque pixels: %1 / %2 (< 50%%). "
"The tooltip is not rendering its background.")
.arg(opaquePixels).arg(totalPixels)));
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
// Top-left 2x2 corner should be fully transparent (rounded corner)
QRect corner(0, 0, 2, 2);
int opaque = countOpaquePixels(img, corner);
QCOMPARE(opaque, 0);
}
void testArrowRendersPixels() {
// Verify the triangle arrow region has some opaque pixels.
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Arrow test");
QVERIFY(tip->isVisible());
QVERIFY(tip->arrowPointsDown());
m_tip->populate("Arrow", "Test", testFont());
// Show below (arrow up) — arrow is in the top strip
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
QVERIFY(m_tip->isVisible());
tip->setWindowOpacity(1.0);
QCoreApplication::processEvents();
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
// Arrow region: below the body rect, centered on arrowLocalX
QRect body = tip->bodyRect();
int arrowTop = body.bottom();
int arrowLeft = tip->arrowLocalX() - RcxTooltip::kArrowHalfW;
int arrowRight = tip->arrowLocalX() + RcxTooltip::kArrowHalfW;
QRect arrowRect(arrowLeft, arrowTop, arrowRight - arrowLeft, RcxTooltip::kArrowH);
int opaquePixels = countOpaquePixels(img, arrowRect);
QVERIFY2(opaquePixels > 0,
qPrintable(QStringLiteral(
"Arrow region has 0 opaque pixels — triangle not painted. "
"arrowRect=(%1,%2 %3x%4) imgSize=(%5x%6)")
.arg(arrowRect.x()).arg(arrowRect.y())
.arg(arrowRect.width()).arg(arrowRect.height())
.arg(img.width()).arg(img.height())));
}
// ──────────────────────────────────────────────────────────────
// LEAVE EVENT RESILIENCE — catches spurious dismiss bugs
// ──────────────────────────────────────────────────────────────
void testSurvivesLeaveEvent() {
// The tooltip should NOT be dismissed when a Leave event fires
// on the trigger widget while the cursor is still in the
// trigger+tooltip zone (simulates the synthetic Leave that Qt
// sends when a tooltip window pops up above the trigger).
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Survive Leave");
QVERIFY(tip->isVisible());
tip->setWindowOpacity(1.0);
// Move real cursor to center of trigger (so geometry check passes)
QPoint trigCenter = m_btnMid->mapToGlobal(
QPoint(m_btnMid->width() / 2, m_btnMid->height() / 2));
QCursor::setPos(trigCenter);
QCoreApplication::processEvents();
// Send a Leave event to the trigger (like DarkApp::notify would)
QEvent leaveEvent(QEvent::Leave);
QApplication::sendEvent(m_btnMid, &leaveEvent);
// Now call scheduleDismiss as DarkApp would
tip->scheduleDismiss();
QCoreApplication::processEvents();
// Tooltip should STILL be visible — cursor is inside trigger zone
QVERIFY2(tip->isVisible(),
"Tooltip was dismissed by spurious Leave event while cursor "
"was still over the trigger widget");
// Wait beyond the dismiss timer to be sure
QTest::qWait(200);
QCoreApplication::processEvents();
QVERIFY2(tip->isVisible(),
"Tooltip was dismissed after 200ms despite cursor being over trigger");
}
void testDismissesOnRealLeave() {
// When the cursor truly leaves the trigger+tooltip zone,
// scheduleDismiss() should queue dismissal and it should fire.
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Real leave");
QVERIFY(tip->isVisible());
tip->setWindowOpacity(1.0);
// Move cursor far away from both trigger and tooltip
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
QCursor::setPos(avail.bottomRight() - QPoint(10, 10));
QCoreApplication::processEvents();
// scheduleDismiss should detect cursor is outside zone
tip->scheduleDismiss();
QCoreApplication::processEvents();
// Wait for the 100ms dismiss timer
QTest::qWait(200);
QCoreApplication::processEvents();
QVERIFY2(!tip->isVisible(),
"Tooltip should have been dismissed when cursor left the zone");
}
void testLeaveAndReshow() {
// Dismiss via real leave, then re-show on a different widget.
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "First");
QVERIFY(tip->isVisible());
// Force dismiss
tip->dismiss();
QCoreApplication::processEvents();
QVERIFY(!tip->isVisible());
// Re-show on different widget
showAndProcess(m_btnLeft, "Second");
QVERIFY2(tip->isVisible(), "Tooltip failed to re-appear after dismiss");
QCOMPARE(tip->currentText(), QString("Second"));
QCOMPARE(tip->currentTrigger(), m_btnLeft);
}
// ── Scheduled dismiss cancelled by new showFor ──
void testScheduledDismissCancelledByShow() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "First");
// Move cursor far away and schedule dismiss
QScreen* scr = QApplication::primaryScreen();
QCursor::setPos(scr->availableGeometry().bottomRight() - QPoint(10, 10));
QCoreApplication::processEvents();
tip->scheduleDismiss();
// Before timer fires, show on a different widget
showAndProcess(m_btnLeft, "Second");
QTest::qWait(200);
QCoreApplication::processEvents();
// Should still be visible — new showFor cancelled the timer
QVERIFY(tip->isVisible());
QCOMPARE(tip->currentText(), QString("Second"));
}
// ── Text change on same widget ──
void testTextChangeOnSameWidget() {
auto* tip = RcxTooltip::instance();
showAndProcess(m_btnMid, "Text A");
QCOMPARE(tip->currentText(), QString("Text A"));
tip->dismiss();
showAndProcess(m_btnMid, "Text B");
QCOMPARE(tip->currentText(), QString("Text B"));
// Arrow region: top kArrowH pixels, centered horizontally
int centerX = img.width() / 2;
QRect arrowRect(centerX - RcxTooltip::kArrowW / 2, 0,
RcxTooltip::kArrowW, RcxTooltip::kArrowH);
int opaque = countOpaquePixels(img, arrowRect);
QVERIFY2(opaque > 0,
qPrintable(QStringLiteral("Arrow region has 0 opaque pixels")));
}
};

View File

@@ -1,290 +1,106 @@
// Tests the full tooltip flow including DarkApp-style ToolTip interception.
// Verifies that QEvent::ToolTip fires and our custom tooltip appears.
// Tests RcxTooltip positioning and arrow direction across screen edges.
// Validates that the arrow tip touches the anchor point and the tooltip
// body stays within screen bounds.
#include <QtTest>
#include <QApplication>
#include <QPushButton>
#include <QScreen>
#include <QHelpEvent>
#include <QImage>
#include "rcxtooltip.h"
#include "themes/thememanager.h"
#include <cstdio>
using namespace rcx;
static void LOG(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stdout, fmt, ap);
va_end(ap);
fflush(stdout);
}
// Simulates DarkApp::notify behavior — installed as a global event filter
class DarkAppSimulator : public QObject {
public:
int tooltipEventCount = 0;
int leaveEventCount = 0;
int showForCallCount = 0;
bool eventFilter(QObject* obj, QEvent* ev) override {
if (ev->type() == QEvent::ToolTip) {
tooltipEventCount++;
if (obj->isWidgetType()) {
auto* w = static_cast<QWidget*>(obj);
QString tip = w->toolTip();
LOG(" [darkapp-sim] ToolTip #%d on '%s' tip='%s'\n",
tooltipEventCount, qPrintable(w->objectName()),
qPrintable(tip.left(60)));
if (!tip.isEmpty()) {
showForCallCount++;
LOG(" [darkapp-sim] calling showFor #%d\n", showForCallCount);
RcxTooltip::instance()->showFor(w, tip);
LOG(" [darkapp-sim] after showFor: visible=%d pos=(%d,%d) size=%dx%d\n",
RcxTooltip::instance()->isVisible(),
RcxTooltip::instance()->x(), RcxTooltip::instance()->y(),
RcxTooltip::instance()->width(), RcxTooltip::instance()->height());
return true; // consume — same as DarkApp
}
}
return true; // suppress default QToolTip
}
if (ev->type() == QEvent::Leave && obj->isWidgetType()) {
auto* tip = RcxTooltip::instance();
if (tip->isVisible() && tip->currentTrigger() == obj) {
leaveEventCount++;
LOG(" [darkapp-sim] Leave #%d on trigger\n", leaveEventCount);
tip->scheduleDismiss();
}
}
return false;
}
};
class TestTooltipEvent : public QObject {
Q_OBJECT
private:
QWidget* m_window = nullptr;
QPushButton* m_btn = nullptr;
QPushButton* m_btn2 = nullptr;
DarkAppSimulator* m_sim = nullptr;
RcxTooltip* m_tip = nullptr;
QFont testFont() {
QFont f("JetBrains Mono", 12);
f.setFixedPitch(true);
return f;
}
private slots:
void initTestCase() {
LOG("=== TestTooltipEvent starting ===\n");
m_window = new QWidget;
m_window->setFixedSize(400, 300);
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
m_window->move(avail.center() - QPoint(200, 150));
m_btn = new QPushButton("Scan", m_window);
m_btn->setToolTip("Start scanning memory");
m_btn->setFixedSize(120, 40);
m_btn->move(30, 130);
m_btn->setObjectName("btnScan");
m_btn2 = new QPushButton("Copy", m_window);
m_btn2->setToolTip("Copy to clipboard");
m_btn2->setFixedSize(120, 40);
m_btn2->move(250, 130);
m_btn2->setObjectName("btnCopy");
// Install DarkApp simulator as global event filter
m_sim = new DarkAppSimulator;
qApp->installEventFilter(m_sim);
m_window->show();
m_window->activateWindow();
m_window->raise();
QVERIFY(QTest::qWaitForWindowExposed(m_window));
// Let window become active
QTest::qWait(200);
QCoreApplication::processEvents();
LOG(" window at (%d,%d)\n", m_window->x(), m_window->y());
LOG(" btn global: (%d,%d)\n",
m_btn->mapToGlobal(QPoint(60, 20)).x(),
m_btn->mapToGlobal(QPoint(60, 20)).y());
m_tip = new RcxTooltip;
const auto& t = ThemeManager::instance().current();
m_tip->setTheme(t.backgroundAlt, t.border, t.text, t.syntaxNumber, t.border);
}
void cleanupTestCase() {
qApp->removeEventFilter(m_sim);
RcxTooltip::instance()->dismiss();
delete m_sim;
delete m_window;
LOG("=== TestTooltipEvent finished ===\n");
m_tip->dismiss();
delete m_tip;
}
void cleanup() {
RcxTooltip::instance()->dismiss();
m_tip->dismiss();
QCoreApplication::processEvents();
m_sim->tooltipEventCount = 0;
m_sim->leaveEventCount = 0;
m_sim->showForCallCount = 0;
}
// Test 1: Post QHelpEvent → DarkApp simulator intercepts → RcxTooltip shows
void testManualEventShowsTooltip() {
LOG("\n--- testManualEventShowsTooltip ---\n");
auto* tip = RcxTooltip::instance();
QPoint btnGlobal = m_btn->mapToGlobal(QPoint(60, 20));
QCursor::setPos(btnGlobal);
// Arrow tip Y matches anchor Y when showing below
void testArrowTipMatchesAnchorBelow() {
m_tip->populate("Test", "Body", testFont());
QScreen* scr = QApplication::primaryScreen();
QPoint anchor = scr->availableGeometry().center();
m_tip->showAt(anchor);
QCoreApplication::processEvents();
LOG(" posting QHelpEvent\n");
QHelpEvent helpEvent(QEvent::ToolTip, QPoint(60, 20), btnGlobal);
QApplication::sendEvent(m_btn, &helpEvent);
QCoreApplication::processEvents();
QTest::qWait(100);
QCoreApplication::processEvents();
LOG(" sim: tooltipEvents=%d showForCalls=%d\n",
m_sim->tooltipEventCount, m_sim->showForCallCount);
LOG(" tip: visible=%d text='%s'\n",
tip->isVisible(), qPrintable(tip->currentText()));
QVERIFY2(m_sim->tooltipEventCount > 0, "Event filter didn't see ToolTip event");
QVERIFY2(m_sim->showForCallCount > 0, "showFor was never called");
QVERIFY2(tip->isVisible(), "RcxTooltip not visible after manual event");
QCOMPARE(tip->currentText(), QString("Start scanning memory"));
// Verify pixels
tip->setWindowOpacity(1.0);
QCoreApplication::processEvents();
QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
QRect body = tip->bodyRect().adjusted(2, 2, -2, -2);
int opaque = 0;
for (int y = body.top(); y <= body.bottom(); ++y)
for (int x = body.left(); x <= body.right(); ++x)
if (qAlpha(img.pixel(x, y)) > 0) opaque++;
LOG(" pixels: %d/%d opaque\n", opaque, body.width() * body.height());
QVERIFY2(opaque > body.width() * body.height() / 2, "Body not rendered");
LOG("--- testManualEventShowsTooltip PASSED ---\n");
QVERIFY(m_tip->isVisible());
// Arrow up (tooltip below): widget top == anchor.y
QCOMPARE(m_tip->y(), anchor.y());
}
// Test 2: Qt's native tooltip timer fires → our filter intercepts → tooltip shows
void testNativeTimerShowsTooltip() {
LOG("\n--- testNativeTimerShowsTooltip ---\n");
auto* tip = RcxTooltip::instance();
// Move cursor away first
QPoint away = m_window->mapToGlobal(QPoint(380, 10));
QCursor::setPos(away);
QTest::qWait(200);
// Arrow tip Y matches anchor Y when showing above
void testArrowTipMatchesAnchorAbove() {
m_tip->populate("Test", "Body", testFont());
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
QPoint anchor(avail.center().x(), avail.bottom() - 2);
m_tip->showAt(anchor);
QCoreApplication::processEvents();
// Move to button
QPoint btnCenter = m_btn->mapToGlobal(QPoint(60, 20));
LOG(" moving cursor to (%d,%d)\n", btnCenter.x(), btnCenter.y());
QCursor::setPos(btnCenter);
// Send Enter + MouseMove to kick the tooltip timer
QEvent enterEv(QEvent::Enter);
QApplication::sendEvent(m_btn, &enterEv);
QMouseEvent moveEv(QEvent::MouseMove, QPointF(60, 20),
m_btn->mapToGlobal(QPointF(60, 20)),
Qt::NoButton, Qt::NoButton, Qt::NoModifier);
QApplication::sendEvent(m_btn, &moveEv);
// Wait up to 2000ms for tooltip to appear
LOG(" waiting for Qt tooltip timer...\n");
bool appeared = false;
for (int i = 0; i < 20; i++) {
QTest::qWait(100);
QCoreApplication::processEvents();
if (m_sim->tooltipEventCount > 0) {
LOG(" tooltip event at ~%dms! events=%d showFor=%d\n",
(i+1)*100, m_sim->tooltipEventCount, m_sim->showForCallCount);
appeared = true;
break;
}
}
// Process remaining events
QTest::qWait(100);
QCoreApplication::processEvents();
LOG(" final: events=%d showFor=%d visible=%d text='%s'\n",
m_sim->tooltipEventCount, m_sim->showForCallCount,
tip->isVisible(), qPrintable(tip->currentText()));
QVERIFY2(appeared, "Qt tooltip timer never fired (no ToolTip event in 2 seconds)");
QVERIFY2(tip->isVisible(), "Tooltip not visible after native timer fired");
LOG("--- testNativeTimerShowsTooltip PASSED ---\n");
QVERIFY(m_tip->isVisible());
// Arrow down (tooltip above): widget bottom == anchor.y
QCOMPARE(m_tip->y() + m_tip->height(), anchor.y());
}
// Test 3: Leave after tooltip shown → tooltip survives (cursor still in zone)
void testLeaveSurvival() {
LOG("\n--- testLeaveSurvival ---\n");
auto* tip = RcxTooltip::instance();
QPoint btnCenter = m_btn->mapToGlobal(QPoint(60, 20));
QCursor::setPos(btnCenter);
// Tooltip stays within screen bounds at left edge
void testScreenLeftEdge() {
m_tip->populate("Test", "Wide body content for edge test", testFont());
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
QPoint anchor(avail.left() + 2, avail.center().y());
m_tip->showAt(anchor);
QCoreApplication::processEvents();
// Show via manual event
QHelpEvent helpEvent(QEvent::ToolTip, QPoint(60, 20), btnCenter);
QApplication::sendEvent(m_btn, &helpEvent);
QCoreApplication::processEvents();
QTest::qWait(100);
QCoreApplication::processEvents();
QVERIFY(tip->isVisible());
// Send Leave (cursor still on button)
LOG(" sending Leave while cursor on button\n");
QEvent leaveEv(QEvent::Leave);
QApplication::sendEvent(m_btn, &leaveEv);
QTest::qWait(200);
QCoreApplication::processEvents();
LOG(" after Leave+200ms: visible=%d leaves=%d\n",
tip->isVisible(), m_sim->leaveEventCount);
QVERIFY2(tip->isVisible(), "Tooltip dismissed by spurious Leave");
LOG("--- testLeaveSurvival PASSED ---\n");
QVERIFY(m_tip->x() >= avail.left());
}
// Test 4: Switch between widgets
void testWidgetSwitch() {
LOG("\n--- testWidgetSwitch ---\n");
auto* tip = RcxTooltip::instance();
// Show on btn1
QPoint btn1Center = m_btn->mapToGlobal(QPoint(60, 20));
QCursor::setPos(btn1Center);
// Tooltip stays within screen bounds at right edge
void testScreenRightEdge() {
m_tip->populate("Test", "Wide body content for edge test", testFont());
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
QPoint anchor(avail.right() - 2, avail.center().y());
m_tip->showAt(anchor);
QCoreApplication::processEvents();
QHelpEvent ev1(QEvent::ToolTip, QPoint(60, 20), btn1Center);
QApplication::sendEvent(m_btn, &ev1);
QCoreApplication::processEvents();
QTest::qWait(100);
QVERIFY(tip->isVisible());
QCOMPARE(tip->currentText(), QString("Start scanning memory"));
QPoint pos1 = tip->pos();
QVERIFY(m_tip->x() + m_tip->width() <= avail.right() + 2);
}
// Switch to btn2
QPoint btn2Center = m_btn2->mapToGlobal(QPoint(60, 20));
QCursor::setPos(btn2Center);
// Content change triggers resize
void testContentResize() {
m_tip->populate("Short", "A", testFont());
m_tip->showAt(QPoint(500, 500));
QCoreApplication::processEvents();
QHelpEvent ev2(QEvent::ToolTip, QPoint(60, 20), btn2Center);
QApplication::sendEvent(m_btn2, &ev2);
int w1 = m_tip->width();
m_tip->dismiss();
m_tip->populate("Much Longer Title", "A much wider body line that should be larger", testFont());
m_tip->showAt(QPoint(500, 500));
QCoreApplication::processEvents();
QTest::qWait(100);
int w2 = m_tip->width();
LOG(" after switch: visible=%d text='%s' pos=(%d,%d)\n",
tip->isVisible(), qPrintable(tip->currentText()),
tip->x(), tip->y());
QVERIFY(tip->isVisible());
QCOMPARE(tip->currentText(), QString("Copy to clipboard"));
QVERIFY(tip->pos() != pos1);
LOG("--- testWidgetSwitch PASSED ---\n");
QVERIFY2(w2 > w1, "Wider content should produce a wider tooltip");
}
};

View File

@@ -1,251 +1,126 @@
// Integration test: simulates the full tooltip flow as DarkApp would see it.
// Posts QHelpEvent (ToolTip), sends Leave events, verifies RcxTooltip behavior
// with fprintf at every stage so we can see exactly what happens.
// Rendering verification for RcxTooltip.
// Grabs widget pixels to confirm WA_TranslucentBackground works correctly
// and the arrow/body are painted with the expected alpha.
#include <QtTest>
#include <QApplication>
#include <QPushButton>
#include <QHelpEvent>
#include <QScreen>
#include <QImage>
#include "rcxtooltip.h"
#include "themes/thememanager.h"
#include <cstdio>
using namespace rcx;
static void LOG(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stdout, fmt, ap);
va_end(ap);
fflush(stdout);
}
// Simulates what DarkApp::notify does when a ToolTip event arrives
static bool simulateDarkAppToolTip(QWidget* w) {
QString tip = w->toolTip();
LOG(" [darkapp] widget='%s' class=%s tip='%s'\n",
qPrintable(w->objectName()), w->metaObject()->className(),
qPrintable(tip));
if (!tip.isEmpty()) {
LOG(" [darkapp] calling RcxTooltip::showFor\n");
RcxTooltip::instance()->showFor(w, tip);
LOG(" [darkapp] showFor returned, visible=%d opacity=%.2f pos=(%d,%d) size=%dx%d\n",
RcxTooltip::instance()->isVisible(),
RcxTooltip::instance()->windowOpacity(),
RcxTooltip::instance()->x(), RcxTooltip::instance()->y(),
RcxTooltip::instance()->width(), RcxTooltip::instance()->height());
return true;
}
return false;
}
// Simulates what DarkApp::notify does when a Leave event arrives
static void simulateDarkAppLeave(QWidget* w) {
auto* tip = RcxTooltip::instance();
if (tip->isVisible() && tip->currentTrigger() == w) {
LOG(" [darkapp] Leave on trigger — calling scheduleDismiss\n");
tip->scheduleDismiss();
LOG(" [darkapp] after scheduleDismiss: visible=%d\n", tip->isVisible());
} else {
LOG(" [darkapp] Leave ignored (visible=%d trigger_match=%d)\n",
tip->isVisible(), tip->currentTrigger() == w);
}
}
class TestTooltipUI : public QObject {
Q_OBJECT
private:
QWidget* m_window = nullptr;
QPushButton* m_btn = nullptr;
QPushButton* m_btn2 = nullptr;
RcxTooltip* m_tip = nullptr;
QFont testFont() {
QFont f("JetBrains Mono", 12);
f.setFixedPitch(true);
return f;
}
int countOpaquePixels(const QImage& img, const QRect& region) {
int count = 0;
QRect r = region.intersected(img.rect());
for (int y = r.top(); y <= r.bottom(); ++y)
for (int x = r.left(); x <= r.right(); ++x)
if (qAlpha(img.pixel(x, y)) > 0)
++count;
return count;
}
private slots:
void initTestCase() {
LOG("=== TestTooltipUI starting ===\n");
m_window = new QWidget;
m_window->setFixedSize(400, 300);
QScreen* scr = QApplication::primaryScreen();
QRect avail = scr->availableGeometry();
m_window->move(avail.center() - QPoint(200, 150));
m_btn = new QPushButton("Scan", m_window);
m_btn->setToolTip("Start scanning memory");
m_btn->setFixedSize(80, 28);
m_btn->move(160, 140);
m_btn->setObjectName("btnScan");
m_btn2 = new QPushButton("Copy", m_window);
m_btn2->setToolTip("Copy address to clipboard");
m_btn2->setFixedSize(80, 28);
m_btn2->move(260, 140);
m_btn2->setObjectName("btnCopy");
m_window->show();
QVERIFY(QTest::qWaitForWindowExposed(m_window));
LOG(" window shown at (%d,%d)\n", m_window->x(), m_window->y());
m_tip = new RcxTooltip;
const auto& t = ThemeManager::instance().current();
m_tip->setTheme(t.backgroundAlt, t.border, t.text, t.syntaxNumber, t.border);
}
void cleanupTestCase() {
RcxTooltip::instance()->dismiss();
delete m_window;
LOG("=== TestTooltipUI finished ===\n");
m_tip->dismiss();
delete m_tip;
}
void cleanup() {
RcxTooltip::instance()->dismiss();
m_tip->dismiss();
QCoreApplication::processEvents();
}
// ─── Test 1: Full tooltip lifecycle with event simulation ───
void testFullLifecycle() {
LOG("\n--- testFullLifecycle ---\n");
auto* tip = RcxTooltip::instance();
// Step 1: Post a ToolTip event (what Qt does after hover delay)
LOG("Step 1: Posting ToolTip event to btn\n");
QPoint btnCenter = m_btn->mapToGlobal(QPoint(40, 14));
LOG(" btn global center: (%d,%d)\n", btnCenter.x(), btnCenter.y());
// Move real cursor to button center
QCursor::setPos(btnCenter);
QCoreApplication::processEvents();
LOG(" cursor moved to button\n");
// Simulate what DarkApp does on ToolTip event
bool handled = simulateDarkAppToolTip(m_btn);
QVERIFY2(handled, "DarkApp should have handled the tooltip");
// Process events (paint, animation start)
QCoreApplication::processEvents();
QTest::qWait(100); // let fade-in animation run
QCoreApplication::processEvents();
LOG("Step 2: Check tooltip state after 100ms\n");
LOG(" visible=%d opacity=%.2f text='%s'\n",
tip->isVisible(), tip->windowOpacity(),
qPrintable(tip->currentText()));
LOG(" pos=(%d,%d) size=%dx%d\n",
tip->x(), tip->y(), tip->width(), tip->height());
LOG(" arrowDown=%d arrowX=%d bodyRect=(%d,%d %dx%d)\n",
tip->arrowPointsDown(), tip->arrowLocalX(),
tip->bodyRect().x(), tip->bodyRect().y(),
tip->bodyRect().width(), tip->bodyRect().height());
QVERIFY2(tip->isVisible(), "Tooltip should be visible after showFor + 100ms");
QCOMPARE(tip->currentText(), QString("Start scanning memory"));
// Step 3: Grab pixels and verify rendering
LOG("Step 3: Verify rendering\n");
tip->setWindowOpacity(1.0);
QCoreApplication::processEvents();
QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
LOG(" grabbed image: %dx%d format=%d\n", img.width(), img.height(), img.format());
int opaquePixels = 0;
QRect body = tip->bodyRect().adjusted(2, 2, -2, -2);
for (int y = body.top(); y <= body.bottom(); ++y)
for (int x = body.left(); x <= body.right(); ++x)
if (qAlpha(img.pixel(x, y)) > 0)
++opaquePixels;
int totalPixels = body.width() * body.height();
LOG(" body opaque pixels: %d / %d (%.1f%%)\n",
opaquePixels, totalPixels,
totalPixels > 0 ? 100.0 * opaquePixels / totalPixels : 0.0);
QVERIFY2(opaquePixels > totalPixels / 2,
qPrintable(QStringLiteral("Only %1/%2 opaque pixels in body — tooltip not rendering")
.arg(opaquePixels).arg(totalPixels)));
// Step 4: Simulate Leave event (spurious — cursor still on button)
LOG("Step 4: Simulate spurious Leave (cursor still on button)\n");
simulateDarkAppLeave(m_btn);
QTest::qWait(200);
QCoreApplication::processEvents();
LOG(" after 200ms: visible=%d\n", tip->isVisible());
QVERIFY2(tip->isVisible(),
"Tooltip dismissed by spurious Leave — geometry check failed");
// Step 5: Move cursor away and simulate real Leave
LOG("Step 5: Move cursor away, simulate real Leave\n");
// Body center should be opaque (background painted)
void testBodyIsOpaque() {
m_tip->populate("Render Test", "Body content here", testFont());
QScreen* scr = QApplication::primaryScreen();
QPoint farAway = scr->availableGeometry().bottomRight() - QPoint(50, 50);
QCursor::setPos(farAway);
QCoreApplication::processEvents();
LOG(" cursor at (%d,%d)\n", farAway.x(), farAway.y());
simulateDarkAppLeave(m_btn);
QTest::qWait(200);
QCoreApplication::processEvents();
LOG(" after 200ms: visible=%d\n", tip->isVisible());
QVERIFY2(!tip->isVisible(),
"Tooltip should be dismissed when cursor truly left the zone");
// Step 6: Re-show on different widget
LOG("Step 6: Re-show on different widget\n");
QPoint btn2Center = m_btn2->mapToGlobal(QPoint(40, 14));
QCursor::setPos(btn2Center);
QCoreApplication::processEvents();
handled = simulateDarkAppToolTip(m_btn2);
QVERIFY(handled);
QCoreApplication::processEvents();
QTest::qWait(100);
QCoreApplication::processEvents();
LOG(" visible=%d text='%s'\n", tip->isVisible(), qPrintable(tip->currentText()));
QVERIFY(tip->isVisible());
QCOMPARE(tip->currentText(), QString("Copy address to clipboard"));
LOG("--- testFullLifecycle PASSED ---\n");
}
// ─── Test 2: Rapid widget switching (no dismiss between) ───
void testRapidSwitch() {
LOG("\n--- testRapidSwitch ---\n");
auto* tip = RcxTooltip::instance();
QCursor::setPos(m_btn->mapToGlobal(QPoint(40, 14)));
QCoreApplication::processEvents();
simulateDarkAppToolTip(m_btn);
m_tip->showAt(scr->availableGeometry().center());
QCoreApplication::processEvents();
QTest::qWait(50);
LOG(" switch to btn2 immediately\n");
QCursor::setPos(m_btn2->mapToGlobal(QPoint(40, 14)));
QCoreApplication::processEvents();
simulateDarkAppToolTip(m_btn2);
QCoreApplication::processEvents();
QTest::qWait(100);
QCoreApplication::processEvents();
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
QVERIFY(!img.isNull());
LOG(" visible=%d text='%s'\n", tip->isVisible(), qPrintable(tip->currentText()));
QVERIFY(tip->isVisible());
QCOMPARE(tip->currentText(), QString("Copy address to clipboard"));
LOG("--- testRapidSwitch PASSED ---\n");
// Center 50% of widget should be mostly opaque
QRect center(img.width() / 4, img.height() / 4,
img.width() / 2, img.height() / 2);
int opaque = countOpaquePixels(img, center);
int total = center.width() * center.height();
QVERIFY2(opaque > total * 0.8,
qPrintable(QStringLiteral("Body has %1/%2 opaque pixels — expected >80%%")
.arg(opaque).arg(total)));
}
// ─── Test 3: Widget with no tooltip ───
void testNoTooltipWidget() {
LOG("\n--- testNoTooltipWidget ---\n");
QPushButton noTip("NoTip", m_window);
noTip.setFixedSize(80, 28);
noTip.move(50, 50);
noTip.show();
// No setToolTip called
// Top-left corner should be transparent (rounded corner + WA_TranslucentBackground)
void testCornerTransparency() {
m_tip->populate("Corner", "Test", testFont());
QScreen* scr = QApplication::primaryScreen();
m_tip->showAt(scr->availableGeometry().center());
QCoreApplication::processEvents();
QTest::qWait(50);
auto* tip = RcxTooltip::instance();
bool handled = simulateDarkAppToolTip(&noTip);
LOG(" handled=%d visible=%d\n", handled, tip->isVisible());
QVERIFY(!handled);
QVERIFY(!tip->isVisible());
LOG("--- testNoTooltipWidget PASSED ---\n");
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
// When arrow is up, body starts at kArrowH. The corner at (0, kArrowH)
// should be transparent due to rounding.
QRect corner(0, 0, 2, 2);
int opaque = countOpaquePixels(img, corner);
QCOMPARE(opaque, 0);
}
// Arrow region should have some opaque pixels
void testArrowHasPixels() {
m_tip->populate("Arrow", "Test", testFont());
QScreen* scr = QApplication::primaryScreen();
m_tip->showAt(scr->availableGeometry().center());
QCoreApplication::processEvents();
QTest::qWait(50);
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
// Arrow is at top (m_up = true): check top kArrowH pixels around center
int cx = img.width() / 2;
QRect arrowRect(cx - RcxTooltip::kArrowW / 2, 0,
RcxTooltip::kArrowW, RcxTooltip::kArrowH);
int opaque = countOpaquePixels(img, arrowRect);
QVERIFY2(opaque > 0, "Arrow region has no opaque pixels");
}
// Grabbing after dismiss should not crash
void testDismissAndReshow() {
m_tip->populate("First", "Body", testFont());
QScreen* scr = QApplication::primaryScreen();
m_tip->showAt(scr->availableGeometry().center());
QCoreApplication::processEvents();
QVERIFY(m_tip->isVisible());
m_tip->dismiss();
QVERIFY(!m_tip->isVisible());
m_tip->populate("Second", "Different", testFont());
m_tip->showAt(scr->availableGeometry().center());
QCoreApplication::processEvents();
QVERIFY(m_tip->isVisible());
}
};