mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
23 Commits
snapshot-1
...
snapshot-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb10bc8a82 | ||
|
|
b5521bd638 | ||
|
|
89d6e1944b | ||
|
|
7528d1bbbb | ||
|
|
4f2288048e | ||
|
|
97b6f55e1f | ||
|
|
6a30e0a402 | ||
|
|
1501a1542c | ||
|
|
4f82b39785 | ||
|
|
009ddc951c | ||
|
|
5921af2b4f | ||
|
|
5ded192990 | ||
|
|
54bee5022b | ||
|
|
5d2d324946 | ||
|
|
5b2cf1ae1f | ||
|
|
f1a36f2ad3 | ||
|
|
665138e688 | ||
|
|
7688bb5b92 | ||
|
|
701e088be8 | ||
|
|
3c0c248d54 | ||
|
|
7af969f6bd | ||
|
|
8ba1fd2492 | ||
|
|
b08736245b |
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -22,6 +22,10 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -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
BIN
docs/README_PIC4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/README_PIC5.png
Normal file
BIN
docs/README_PIC5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
63
plugins/KernelMemory/CMakeLists.txt
Normal file
63
plugins/KernelMemory/CMakeLists.txt
Normal 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
|
||||
)
|
||||
751
plugins/KernelMemory/KernelMemoryPlugin.cpp
Normal file
751
plugins/KernelMemory/KernelMemoryPlugin.cpp
Normal 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();
|
||||
}
|
||||
142
plugins/KernelMemory/KernelMemoryPlugin.h
Normal file
142
plugins/KernelMemory/KernelMemoryPlugin.h
Normal 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();
|
||||
99
plugins/KernelMemory/driver/build_driver.bat
Normal file
99
plugins/KernelMemory/driver/build_driver.bat
Normal 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
|
||||
808
plugins/KernelMemory/driver/rcxdrv.c
Normal file
808
plugins/KernelMemory/driver/rcxdrv.c
Normal 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;
|
||||
}
|
||||
17
plugins/KernelMemory/linux/Makefile
Normal file
17
plugins/KernelMemory/linux/Makefile
Normal 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
|
||||
132
plugins/KernelMemory/linux/rcxkm.c
Normal file
132
plugins/KernelMemory/linux/rcxkm.c
Normal 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);
|
||||
189
plugins/KernelMemory/rcx_drv_protocol.h
Normal file
189
plugins/KernelMemory/rcx_drv_protocol.h
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 '['
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
src/core.h
20
src/core.h
@@ -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
|
||||
|
||||
161
src/editor.cpp
161
src/editor.cpp
@@ -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);
|
||||
|
||||
@@ -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
126
src/examples/PageTables.rcx
Normal 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"]
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
193
src/imports/pe_debug_info.cpp
Normal file
193
src/imports/pe_debug_info.cpp
Normal 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
|
||||
20
src/imports/pe_debug_info.h
Normal file
20
src/imports/pe_debug_info.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
1125
src/main.cpp
1125
src/main.cpp
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -57,6 +64,12 @@ 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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
342
src/rcxtooltip.h
342
src/rcxtooltip.h
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
123
src/symbol_downloader.cpp
Normal 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
50
src/symbol_downloader.h
Normal 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
171
src/symbolstore.cpp
Normal 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
95
src/symbolstore.h
Normal 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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; });
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
222
tests/grab_tabs.cpp
Normal 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"
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()});
|
||||
}
|
||||
}
|
||||
|
||||
397
tests/test_kernel_provider.cpp
Normal file
397
tests/test_kernel_provider.cpp
Normal 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"
|
||||
@@ -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
143
tests/test_project_dock.cpp
Normal 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"
|
||||
307
tests/test_roundtrip_winsdk.cpp
Normal file
307
tests/test_roundtrip_winsdk.cpp
Normal 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"
|
||||
@@ -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);
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user