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"
|
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||||
cmake --build build
|
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
|
- name: Test
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -62,6 +92,7 @@ jobs:
|
|||||||
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
|
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
|
||||||
mkdir -p release/Plugins
|
mkdir -p release/Plugins
|
||||||
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
|
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/themes release/ 2>/dev/null || true
|
||||||
cp -r build/examples release/ 2>/dev/null || true
|
cp -r build/examples release/ 2>/dev/null || true
|
||||||
cp build/screenshot.png 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/mcp/mcp_bridge.cpp
|
||||||
src/addressparser.h
|
src/addressparser.h
|
||||||
src/addressparser.cpp
|
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.h
|
||||||
src/disasm.cpp
|
src/disasm.cpp
|
||||||
third_party/fadec/decode.c
|
third_party/fadec/decode.c
|
||||||
@@ -415,7 +421,7 @@ if(BUILD_TESTING)
|
|||||||
if(BUILD_UI_TESTS)
|
if(BUILD_UI_TESTS)
|
||||||
|
|
||||||
add_executable(test_controller tests/test_controller.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_controller COMMAND test_controller)
|
||||||
|
|
||||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_context_menu COMMAND test_context_menu)
|
||||||
|
|
||||||
add_executable(test_source_management tests/test_source_management.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||||
|
|
||||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_type_selector COMMAND test_type_selector)
|
||||||
|
|
||||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||||
|
|
||||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
||||||
@@ -544,6 +550,17 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_windbg_provider PRIVATE
|
target_link_libraries(test_windbg_provider PRIVATE
|
||||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
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()
|
endif()
|
||||||
|
|
||||||
add_executable(bench_large_class tests/bench_large_class.cpp
|
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||||
@@ -587,6 +604,7 @@ if(NOT APPLE)
|
|||||||
add_subdirectory(plugins/RemoteProcessMemory)
|
add_subdirectory(plugins/RemoteProcessMemory)
|
||||||
endif()
|
endif()
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
add_subdirectory(plugins/KernelMemory)
|
||||||
add_subdirectory(plugins/WinDbgMemory)
|
add_subdirectory(plugins/WinDbgMemory)
|
||||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor
|
|||||||
|
|
||||||
## Screenshots
|
## 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
|
- **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)
|
- **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
|
- **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
|
- **WinDbg** — connect to live WinDbg debugging sessions or load crash dumps
|
||||||
- **Saved sources** — quick-switch between recently used data sources per tab
|
- **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 |
|
| Plugin | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| **Process memory** | Attach to local processes on Windows and Linux — PID-based, with symbol resolution and module/region enumeration |
|
| **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 |
|
| **WinDbg** | Access data from live WinDbg debugging sessions |
|
||||||
| **Remote process memory** | TCP RPC-based remote process access with cross-architecture support |
|
| **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 |
|
| **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 )
|
if ( i == 0 )
|
||||||
m_base = (uint64_t)mi.lpBaseOfDll;
|
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),
|
QString::fromWCharArray(modName),
|
||||||
|
fullPath,
|
||||||
(uint64_t)mi.lpBaseOfDll,
|
(uint64_t)mi.lpBaseOfDll,
|
||||||
(uint64_t)mi.SizeOfImage
|
(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> ProcessMemoryProvider::enumerateRegions() const
|
||||||
{
|
{
|
||||||
QVector<rcx::MemoryRegion> regions;
|
QVector<rcx::MemoryRegion> regions;
|
||||||
@@ -400,8 +415,9 @@ void ProcessMemoryProvider::cacheModules()
|
|||||||
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
|
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
|
||||||
{
|
{
|
||||||
QFileInfo fi(it.key());
|
QFileInfo fi(it.key());
|
||||||
m_modules.append({
|
m_modules.push_back(ModuleInfo{
|
||||||
fi.fileName(),
|
fi.fileName(),
|
||||||
|
it.key(),
|
||||||
it->base,
|
it->base,
|
||||||
it->end - it->base
|
it->end - it->base
|
||||||
});
|
});
|
||||||
@@ -530,7 +546,7 @@ QVector<rcx::Provider::ThreadInfo> ProcessMemoryProvider::tebs() const
|
|||||||
ULONG tbiLen = 0;
|
ULONG tbiLen = 0;
|
||||||
NTSTATUS qitSt = pNtQIT(hThread, 0, &tbi, sizeof(tbi), &tbiLen);
|
NTSTATUS qitSt = pNtQIT(hThread, 0, &tbi, sizeof(tbi), &tbiLen);
|
||||||
if (qitSt >= 0 && tbi.TebBaseAddress)
|
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);
|
CloseHandle(hThread);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public:
|
|||||||
void refreshModules() { m_modules.clear(); cacheModules(); }
|
void refreshModules() { m_modules.clear(); cacheModules(); }
|
||||||
uint64_t peb() const override { return m_peb; }
|
uint64_t peb() const override { return m_peb; }
|
||||||
QVector<ThreadInfo> tebs() const override;
|
QVector<ThreadInfo> tebs() const override;
|
||||||
|
QVector<ModuleEntry> enumerateModules() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void cacheModules();
|
void cacheModules();
|
||||||
@@ -62,6 +63,7 @@ private:
|
|||||||
|
|
||||||
struct ModuleInfo {
|
struct ModuleInfo {
|
||||||
QString name;
|
QString name;
|
||||||
|
QString fullPath;
|
||||||
uint64_t base;
|
uint64_t base;
|
||||||
uint64_t size;
|
uint64_t size;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ struct IpcClient {
|
|||||||
reinterpret_cast<const char*>(data + entry->nameOffset),
|
reinterpret_cast<const char*>(data + entry->nameOffset),
|
||||||
(int)entry->nameLength);
|
(int)entry->nameLength);
|
||||||
#endif
|
#endif
|
||||||
result.append({modName, entry->base, entry->size});
|
result.push_back(RemoteProcessProvider::ModuleInfo{modName, entry->base, entry->size});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace rcx {
|
|||||||
//
|
//
|
||||||
// All numeric literals are hexadecimal (base 16).
|
// All numeric literals are hexadecimal (base 16).
|
||||||
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
|
// 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.
|
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
|
||||||
|
|
||||||
class ExpressionParser {
|
class ExpressionParser {
|
||||||
@@ -273,16 +274,46 @@ private:
|
|||||||
// Identifier or hex literal disambiguation.
|
// Identifier or hex literal disambiguation.
|
||||||
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
|
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
|
||||||
// Otherwise → backtrack and parse as hex number.
|
// 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) {
|
bool parseIdentifierOrHex(uint64_t& result) {
|
||||||
int start = m_pos;
|
int start = m_pos;
|
||||||
bool hasNonHex = false;
|
bool hasNonHex = false;
|
||||||
|
|
||||||
// Scan full token
|
// Scan full token, including "module!symbol" as one token
|
||||||
while (!atEnd() && isIdentChar(peek())) {
|
while (!atEnd() && isIdentChar(peek())) {
|
||||||
if (!isHexDigit(peek()))
|
if (!isHexDigit(peek()))
|
||||||
hasNonHex = true;
|
hasNonHex = true;
|
||||||
advance();
|
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);
|
QString token = m_input.mid(start, m_pos - start);
|
||||||
|
|
||||||
@@ -292,6 +323,11 @@ private:
|
|||||||
return parseHexNumber(result);
|
return parseHexNumber(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for function call syntax: identifier '(' args ')'
|
||||||
|
skipSpaces();
|
||||||
|
if (peek() == '(')
|
||||||
|
return parseFunctionCall(token, result);
|
||||||
|
|
||||||
// It's an identifier — resolve via callback
|
// It's an identifier — resolve via callback
|
||||||
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
|
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
|
||||||
result = 0;
|
result = 0;
|
||||||
@@ -305,6 +341,71 @@ private:
|
|||||||
return true;
|
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
|
// '[' bitwiseOr ']' — read the pointer value at the computed address
|
||||||
bool parseDereference(uint64_t& result) {
|
bool parseDereference(uint64_t& result) {
|
||||||
advance(); // skip '['
|
advance(); // skip '['
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ struct AddressParserCallbacks {
|
|||||||
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
||||||
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
|
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
|
||||||
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
|
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 {
|
class AddressParser {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ struct ComposeState {
|
|||||||
bool treeLines = false; // draw Unicode tree connectors in indentation
|
bool treeLines = false; // draw Unicode tree connectors in indentation
|
||||||
bool braceWrap = false; // opening brace on its own line
|
bool braceWrap = false; // opening brace on its own line
|
||||||
bool typeHints = false; // show type inference hints on hex nodes
|
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
|
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
|
||||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
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(
|
auto suggestions = inferTypes(
|
||||||
reinterpret_cast<const uint8_t*>(b.constData()), sz);
|
reinterpret_cast<const uint8_t*>(b.constData()), sz);
|
||||||
if (!suggestions.isEmpty() && suggestions[0].strength >= 3) {
|
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;
|
lm.typeHintKinds = suggestions[0].kinds;
|
||||||
QString typeName = formatHint(suggestions[0]);
|
QString typeName = formatHint(suggestions[0]);
|
||||||
QString preview = formatPreview(
|
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));
|
state.emitLine(lineText, std::move(lm));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -695,6 +703,11 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
*ok = false;
|
*ok = false;
|
||||||
return 0;
|
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;
|
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: "};"
|
// Footer line: "};"
|
||||||
{
|
{
|
||||||
LineMeta flm;
|
LineMeta flm;
|
||||||
@@ -893,6 +943,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
&& node.refId != 0) {
|
&& node.refId != 0) {
|
||||||
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||||
|
if (node.isRelative)
|
||||||
|
ptrTypeOverride += QStringLiteral(" rva");
|
||||||
|
|
||||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||||
const QVector<int>& ptrChildren = childIndices(state, node.id);
|
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;
|
uint64_t pBase = ptrVal;
|
||||||
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
|
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,
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||||
bool compactColumns, bool treeLines, bool braceWrap,
|
bool compactColumns, bool treeLines, bool braceWrap,
|
||||||
bool typeHints) {
|
bool typeHints, SymbolLookupFn symbolLookup) {
|
||||||
ComposeState state;
|
ComposeState state;
|
||||||
state.compactColumns = compactColumns;
|
state.compactColumns = compactColumns;
|
||||||
state.treeLines = treeLines;
|
state.treeLines = treeLines;
|
||||||
state.braceWrap = braceWrap;
|
state.braceWrap = braceWrap;
|
||||||
state.typeHints = typeHints;
|
state.typeHints = typeHints;
|
||||||
|
state.symbolLookup = std::move(symbolLookup);
|
||||||
|
|
||||||
// Precompute parent→children map
|
// Precompute parent→children map
|
||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "addressparser.h"
|
#include "addressparser.h"
|
||||||
|
#include "symbolstore.h"
|
||||||
#include "typeselectorpopup.h"
|
#include "typeselectorpopup.h"
|
||||||
#include "providerregistry.h"
|
#include "providerregistry.h"
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QtConcurrent/QtConcurrentRun>
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
@@ -73,8 +75,10 @@ RcxDocument::RcxDocument(QObject* parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
||||||
bool treeLines, bool braceWrap, bool typeHints) const {
|
bool treeLines, bool braceWrap, bool typeHints,
|
||||||
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap, typeHints);
|
SymbolLookupFn symbolLookup) const {
|
||||||
|
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap, typeHints,
|
||||||
|
std::move(symbolLookup));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RcxDocument::save(const QString& path) {
|
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
|
// Footer "Trim" button — remove trailing hex nodes from end of struct
|
||||||
connect(editor, &RcxEditor::trimHexRequested,
|
connect(editor, &RcxEditor::trimHexRequested,
|
||||||
this, [this](uint64_t structId) {
|
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);
|
QVector<int> children = m_doc->tree.childrenOf(structId);
|
||||||
if (children.isEmpty()) return;
|
if (children.isEmpty()) return;
|
||||||
|
|
||||||
@@ -303,7 +311,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
|
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
|
||||||
auto oldMembers = members;
|
auto oldMembers = members;
|
||||||
for (int i = 0; i < count; i++)
|
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,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeEnumMembers{enumId, oldMembers, members}));
|
cmd::ChangeEnumMembers{enumId, oldMembers, members}));
|
||||||
});
|
});
|
||||||
@@ -441,13 +449,38 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
*ok = prov->read(addr, &val, ptrSz);
|
*ok = prov->read(addr, &val, ptrSz);
|
||||||
return val;
|
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);
|
auto result = AddressParser::evaluate(s, m_doc->tree.pointerSize, &cbs);
|
||||||
if (result.ok && result.value != m_doc->tree.baseAddress) {
|
if (result.ok && result.value != m_doc->tree.baseAddress) {
|
||||||
uint64_t oldBase = m_doc->tree.baseAddress;
|
uint64_t oldBase = m_doc->tree.baseAddress;
|
||||||
QString oldFormula = m_doc->tree.baseAddressFormula;
|
QString oldFormula = m_doc->tree.baseAddressFormula;
|
||||||
// Store formula if input uses module/deref syntax, otherwise clear
|
// Store formula if input uses module/deref/kernel-function/symbol syntax
|
||||||
QString newFormula = (s.contains('<') || s.contains('[')) ? s : QString();
|
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,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula}));
|
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
|
// Bracket compose with thread-local doc pointer for type name resolution
|
||||||
s_composeDoc = m_doc;
|
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
|
// Compose against snapshot provider if active, otherwise real provider
|
||||||
if (m_snapshotProv)
|
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
|
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;
|
s_composeDoc = nullptr;
|
||||||
|
|
||||||
@@ -621,6 +663,7 @@ void RcxController::refresh() {
|
|||||||
for (auto& lm : m_lastResult.meta) {
|
for (auto& lm : m_lastResult.meta) {
|
||||||
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
||||||
int64_t offset = m_doc->tree.computeOffset(lm.nodeIdx);
|
int64_t offset = m_doc->tree.computeOffset(lm.nodeIdx);
|
||||||
|
if (offset < 0) continue;
|
||||||
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
||||||
|
|
||||||
if (isHexPreview(node.kind)) {
|
if (isHexPreview(node.kind)) {
|
||||||
@@ -813,7 +856,7 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
|||||||
if (si == nodeIdx) continue;
|
if (si == nodeIdx) continue;
|
||||||
auto& sib = m_doc->tree.nodes[si];
|
auto& sib = m_doc->tree.nodes[si];
|
||||||
if (sib.offset >= oldEnd)
|
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);
|
bool needsRename = isHexNode(node.kind) && !isHexNode(newKind);
|
||||||
@@ -887,7 +930,7 @@ void RcxController::insertNodeAbove(int beforeIdx, NodeKind kind, const QString&
|
|||||||
for (int si : siblings) {
|
for (int si : siblings) {
|
||||||
auto& sib = m_doc->tree.nodes[si];
|
auto& sib = m_doc->tree.nodes[si];
|
||||||
if (sib.offset >= before.offset)
|
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}));
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
|
||||||
@@ -912,7 +955,7 @@ void RcxController::removeNode(int nodeIdx) {
|
|||||||
if (si == nodeIdx) continue;
|
if (si == nodeIdx) continue;
|
||||||
auto& sib = m_doc->tree.nodes[si];
|
auto& sib = m_doc->tree.nodes[si];
|
||||||
if (sib.offset >= deletedEnd) {
|
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;
|
if (si == nodeIdx) continue;
|
||||||
auto& sib = m_doc->tree.nodes[si];
|
auto& sib = m_doc->tree.nodes[si];
|
||||||
if (sib.offset >= copyOffset)
|
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;
|
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
|
||||||
|
|
||||||
const auto& bm = node.bitfieldMembers[memberIdx];
|
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);
|
int containerSize = sizeForKind(node.elementKind);
|
||||||
if (containerSize <= 0) containerSize = 4;
|
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;
|
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
|
||||||
|
|
||||||
const auto& bm = node.bitfieldMembers[memberIdx];
|
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);
|
int containerSize = sizeForKind(node.elementKind);
|
||||||
if (containerSize <= 0) containerSize = 4;
|
if (containerSize <= 0) containerSize = 4;
|
||||||
|
|
||||||
@@ -1739,14 +1786,24 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
// ── Insert ► submenu ──
|
// ── 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");
|
auto* insertMenu = menu.addMenu(icon("diff-added.svg"), "Insert");
|
||||||
insertMenu->addAction("Insert 4", [this]() {
|
insertMenu->addAction("Insert 4 Above", [this, firstIdx]() {
|
||||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
if (firstIdx >= 0)
|
||||||
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
insertNodeAbove(firstIdx, NodeKind::Hex32, QStringLiteral("field"));
|
||||||
});
|
});
|
||||||
insertMenu->addAction("Insert 8", [this]() {
|
insertMenu->addAction("Insert 8 Above", [this, firstIdx]() {
|
||||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
if (firstIdx >= 0)
|
||||||
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
insertNodeAbove(firstIdx, NodeKind::Hex64, QStringLiteral("field"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1812,7 +1869,9 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
for (uint64_t id : ids) {
|
for (uint64_t id : ids) {
|
||||||
int ni = m_doc->tree.indexOfId(id);
|
int ni = m_doc->tree.indexOfId(id);
|
||||||
if (ni < 0) continue;
|
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();
|
addrs << QStringLiteral("0x") + QString::number(addr, 16).toUpper();
|
||||||
}
|
}
|
||||||
QApplication::clipboard()->setText(addrs.join('\n'));
|
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;
|
auto members = m_doc->tree.nodes[ni].enumMembers;
|
||||||
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
|
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
|
||||||
auto oldMembers = members;
|
auto oldMembers = members;
|
||||||
members.append({QStringLiteral("NewMember"), nextVal});
|
members.emplaceBack(QStringLiteral("NewMember"), nextVal);
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
|
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]() {
|
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, copyNodeId]() {
|
||||||
int ni = m_doc->tree.indexOfId(copyNodeId);
|
int ni = m_doc->tree.indexOfId(copyNodeId);
|
||||||
if (ni < 0) return;
|
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(
|
QApplication::clipboard()->setText(
|
||||||
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
|
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);
|
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);
|
emit contextMenuAboutToShow(&menu, line);
|
||||||
menu.exec(globalPos);
|
menu.exec(globalPos);
|
||||||
}
|
}
|
||||||
@@ -3208,6 +3369,29 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
|||||||
*ok = prov->read(addr, &val, ptrSz);
|
*ok = prov->read(addr, &val, ptrSz);
|
||||||
return val;
|
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);
|
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
||||||
if (result.ok)
|
if (result.ok)
|
||||||
m_doc->tree.baseAddress = result.value;
|
m_doc->tree.baseAddress = result.value;
|
||||||
@@ -3330,6 +3514,29 @@ void RcxController::selectSource(const QString& text) {
|
|||||||
*ok = prov->read(addr, &val, ptrSz);
|
*ok = prov->read(addr, &val, ptrSz);
|
||||||
return val;
|
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(
|
auto result = AddressParser::evaluate(
|
||||||
m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
||||||
if (result.ok)
|
if (result.ok)
|
||||||
@@ -3456,7 +3663,7 @@ void RcxController::collectPointerRanges(
|
|||||||
|
|
||||||
int span = m_doc->tree.structSpan(structId);
|
int span = m_doc->tree.structSpan(structId);
|
||||||
if (span <= 0) return;
|
if (span <= 0) return;
|
||||||
ranges.append({memBase, span});
|
ranges.emplaceBack(memBase, span);
|
||||||
|
|
||||||
if (!m_snapshotProv) return;
|
if (!m_snapshotProv) return;
|
||||||
|
|
||||||
@@ -3504,7 +3711,7 @@ void RcxController::onRefreshTick() {
|
|||||||
|
|
||||||
// Collect all needed ranges: main struct + pointer targets (absolute addresses)
|
// Collect all needed ranges: main struct + pointer targets (absolute addresses)
|
||||||
QVector<QPair<uint64_t,int>> ranges;
|
QVector<QPair<uint64_t,int>> ranges;
|
||||||
ranges.append({m_doc->tree.baseAddress, extent});
|
ranges.emplaceBack(m_doc->tree.baseAddress, extent);
|
||||||
|
|
||||||
if (m_snapshotProv) {
|
if (m_snapshotProv) {
|
||||||
QSet<QPair<uint64_t,uint64_t>> visited;
|
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++) {
|
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||||
const Node& node = m_doc->tree.nodes[i];
|
const Node& node = m_doc->tree.nodes[i];
|
||||||
int64_t off = m_doc->tree.computeOffset(i);
|
int64_t off = m_doc->tree.computeOffset(i);
|
||||||
|
if (off < 0) continue;
|
||||||
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
||||||
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
||||||
int64_t end = off + sz;
|
int64_t end = off + sz;
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ public:
|
|||||||
|
|
||||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
||||||
bool treeLines = false, bool braceWrap = false,
|
bool treeLines = false, bool braceWrap = false,
|
||||||
bool typeHints = false) const;
|
bool typeHints = false,
|
||||||
|
SymbolLookupFn symbolLookup = {}) const;
|
||||||
bool save(const QString& path);
|
bool save(const QString& path);
|
||||||
bool load(const QString& path);
|
bool load(const QString& path);
|
||||||
void loadData(const QString& binaryPath);
|
void loadData(const QString& binaryPath);
|
||||||
@@ -163,6 +164,8 @@ signals:
|
|||||||
void nodeSelected(int nodeIdx);
|
void nodeSelected(int nodeIdx);
|
||||||
void selectionChanged(int count);
|
void selectionChanged(int count);
|
||||||
void contextMenuAboutToShow(QMenu* menu, int line);
|
void contextMenuAboutToShow(QMenu* menu, int line);
|
||||||
|
void requestOpenProviderTab(const QString& pluginId, const QString& target,
|
||||||
|
const QString& title);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RcxDocument* m_doc;
|
RcxDocument* m_doc;
|
||||||
|
|||||||
20
src/core.h
20
src/core.h
@@ -197,6 +197,7 @@ struct Node {
|
|||||||
int offset = 0;
|
int offset = 0;
|
||||||
bool isStatic = false; // static field — excluded from struct layout
|
bool isStatic = false; // static field — excluded from struct layout
|
||||||
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
|
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 arrayLen = 1; // Array: element count
|
||||||
int strLen = 64;
|
int strLen = 64;
|
||||||
bool collapsed = true;
|
bool collapsed = true;
|
||||||
@@ -242,6 +243,8 @@ struct Node {
|
|||||||
o["isStatic"] = true;
|
o["isStatic"] = true;
|
||||||
if (!offsetExpr.isEmpty())
|
if (!offsetExpr.isEmpty())
|
||||||
o["offsetExpr"] = offsetExpr;
|
o["offsetExpr"] = offsetExpr;
|
||||||
|
if (isRelative)
|
||||||
|
o["isRelative"] = true;
|
||||||
o["arrayLen"] = arrayLen;
|
o["arrayLen"] = arrayLen;
|
||||||
o["strLen"] = strLen;
|
o["strLen"] = strLen;
|
||||||
o["collapsed"] = collapsed;
|
o["collapsed"] = collapsed;
|
||||||
@@ -283,9 +286,10 @@ struct Node {
|
|||||||
n.offset = o["offset"].toInt(0);
|
n.offset = o["offset"].toInt(0);
|
||||||
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
|
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
|
||||||
n.offsetExpr = o["offsetExpr"].toString();
|
n.offsetExpr = o["offsetExpr"].toString();
|
||||||
|
n.isRelative = o["isRelative"].toBool(false);
|
||||||
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
|
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
|
||||||
n.strLen = qBound(1, o["strLen"].toInt(64), 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.refId = o["refId"].toString("0").toULongLong();
|
||||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||||
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||||
@@ -293,8 +297,8 @@ struct Node {
|
|||||||
QJsonArray arr = o["enumMembers"].toArray();
|
QJsonArray arr = o["enumMembers"].toArray();
|
||||||
for (const auto& v : arr) {
|
for (const auto& v : arr) {
|
||||||
QJsonObject em = v.toObject();
|
QJsonObject em = v.toObject();
|
||||||
n.enumMembers.append({em["name"].toString(),
|
n.enumMembers.emplaceBack(em["name"].toString(),
|
||||||
em["value"].toString("0").toLongLong()});
|
em["value"].toString("0").toLongLong());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (o.contains("bitfieldMembers")) {
|
if (o.contains("bitfieldMembers")) {
|
||||||
@@ -677,6 +681,7 @@ namespace cmd {
|
|||||||
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
|
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
|
||||||
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
|
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
|
||||||
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
|
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
|
||||||
|
struct ToggleRelative { uint64_t nodeId; bool oldVal, newVal; };
|
||||||
}
|
}
|
||||||
|
|
||||||
using Command = std::variant<
|
using Command = std::variant<
|
||||||
@@ -684,7 +689,7 @@ using Command = std::variant<
|
|||||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
||||||
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
|
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
|
||||||
cmd::ChangeOffsetExpr, cmd::ToggleStatic
|
cmd::ChangeOffsetExpr, cmd::ToggleStatic, cmd::ToggleRelative
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// ── Column spans (for inline editing) ──
|
// ── Column spans (for inline editing) ──
|
||||||
@@ -1038,8 +1043,13 @@ namespace fmt {
|
|||||||
|
|
||||||
// ── Compose function forward declaration ──
|
// ── 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,
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||||
bool compactColumns = false, bool treeLines = false,
|
bool compactColumns = false, bool treeLines = false,
|
||||||
bool braceWrap = false, bool typeHints = false);
|
bool braceWrap = false, bool typeHints = false,
|
||||||
|
SymbolLookupFn symbolLookup = {});
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
161
src/editor.cpp
161
src/editor.cpp
@@ -1,6 +1,7 @@
|
|||||||
#include "editor.h"
|
#include "editor.h"
|
||||||
#include "disasm.h"
|
#include "disasm.h"
|
||||||
#include "providerregistry.h"
|
#include "providerregistry.h"
|
||||||
|
#include "rcxtooltip.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <Qsci/qsciscintilla.h>
|
#include <Qsci/qsciscintilla.h>
|
||||||
#include <Qsci/qsciscintillabase.h>
|
#include <Qsci/qsciscintillabase.h>
|
||||||
@@ -1397,6 +1398,7 @@ void RcxEditor::dismissAllPopups() {
|
|||||||
if (m_historyPopup) static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
if (m_historyPopup) static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||||
if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
|
if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
|
||||||
if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
|
if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
|
||||||
|
if (m_arrowTooltip) static_cast<RcxTooltip*>(m_arrowTooltip)->dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::hideFindBar() {
|
void RcxEditor::hideFindBar() {
|
||||||
@@ -2377,12 +2379,6 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|||||||
int line, col;
|
int line, col;
|
||||||
m_sci->getCursorPosition(&line, &col);
|
m_sci->getCursorPosition(&line, &col);
|
||||||
int minCol = m_editState.spanStart;
|
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 there's an active selection, collapse it to the left end (Left only, not Backspace)
|
||||||
if (ke->key() == Qt::Key_Left) {
|
if (ke->key() == Qt::Key_Left) {
|
||||||
int sL, sC, eL, eC;
|
int sL, sC, eL, eC;
|
||||||
@@ -2410,17 +2406,9 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|||||||
if (col >= editEndCol()) return true; // block past end
|
if (col >= editEndCol()) return true; // block past end
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case Qt::Key_Home: {
|
case Qt::Key_Home:
|
||||||
int home = m_editState.spanStart;
|
m_sci->setCursorPosition(m_editState.line, 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);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
case Qt::Key_End:
|
case Qt::Key_End:
|
||||||
m_sci->setCursorPosition(m_editState.line, editEndCol());
|
m_sci->setCursorPosition(m_editState.line, editEndCol());
|
||||||
return true;
|
return true;
|
||||||
@@ -2865,21 +2853,21 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
|| target == EditTarget::PointerTarget
|
|| target == EditTarget::PointerTarget
|
||||||
|| target == EditTarget::RootClassType);
|
|| target == EditTarget::RootClassType);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
||||||
if (!isPicker)
|
if (!isPicker) {
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
|
// Subtle tint derived from theme background (neutral, not blue)
|
||||||
ThemeManager::instance().current().selection);
|
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!)
|
// Use correct UTF-8 position conversion (not lineStart + col!)
|
||||||
m_editState.posStart = posFromCol(m_sci, line, norm.start);
|
m_editState.posStart = posFromCol(m_sci, line, norm.start);
|
||||||
m_editState.posEnd = posFromCol(m_sci, line, norm.end);
|
m_editState.posEnd = posFromCol(m_sci, line, norm.end);
|
||||||
|
|
||||||
// For Value/BaseAddress: skip 0x prefix in selection (select only the number)
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, m_editState.posStart, m_editState.posEnd);
|
||||||
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);
|
|
||||||
|
|
||||||
// Hex overwrite: place cursor at start, no selection
|
// Hex overwrite: place cursor at start, no selection
|
||||||
if (m_editState.hexOverwrite)
|
if (m_editState.hexOverwrite)
|
||||||
@@ -2893,8 +2881,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
|
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
|
||||||
} else if (target == EditTarget::Name && m_editState.hexOverwrite) {
|
} else if (target == EditTarget::Name && m_editState.hexOverwrite) {
|
||||||
setEditComment(QStringLiteral("ASCII edit: Enter=Save Esc=Cancel"));
|
setEditComment(QStringLiteral("ASCII edit: Enter=Save Esc=Cancel"));
|
||||||
} else if (target == EditTarget::BaseAddress)
|
} else if (target == EditTarget::BaseAddress) {
|
||||||
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
|
// No inline hint — the hover tooltip already shows examples
|
||||||
|
}
|
||||||
|
|
||||||
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
|
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
|
||||||
// and exit early above (never reach here).
|
// and exit early above (never reach here).
|
||||||
@@ -3062,26 +3051,8 @@ void RcxEditor::showSourcePicker() {
|
|||||||
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
||||||
menuFont.setPointSize(menuFont.pointSize() + zoom);
|
menuFont.setPointSize(menuFont.pointSize() + zoom);
|
||||||
menu.setFont(menuFont);
|
menu.setFont(menuFont);
|
||||||
menu.addAction("File");
|
|
||||||
|
|
||||||
// Add all registered providers from global registry
|
ProviderRegistry::populateSourceMenu(&menu, m_savedSourceDisplay);
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
||||||
int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||||
@@ -3095,11 +3066,13 @@ void RcxEditor::showSourcePicker() {
|
|||||||
const LineMeta* lm = metaForLine(m_editState.line);
|
const LineMeta* lm = metaForLine(m_editState.line);
|
||||||
uint64_t addr = lm ? lm->offsetAddr : 0;
|
uint64_t addr = lm ? lm->offsetAddr : 0;
|
||||||
auto info = endInlineEdit();
|
auto info = endInlineEdit();
|
||||||
QString text = sel->text();
|
// Route via action data (set by populateSourceMenu)
|
||||||
if (sel->data().toString() == QStringLiteral("#clear"))
|
QString text = sel->data().toString();
|
||||||
text = QStringLiteral("#clear");
|
if (text.isEmpty()) {
|
||||||
else if (sel->data().isValid())
|
// Plugin action (e.g. "Unload Driver") — already handled by its own lambda
|
||||||
text = QStringLiteral("#saved:") + QString::number(sel->data().toInt());
|
cancelInlineEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
|
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
|
||||||
} else {
|
} else {
|
||||||
cancelInlineEdit();
|
cancelInlineEdit();
|
||||||
@@ -3794,6 +3767,82 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
// else: desired stays Arrow (hovering over column padding)
|
// 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);
|
m_sci->viewport()->setCursor(desired);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3860,12 +3909,8 @@ void RcxEditor::validateEditLive() {
|
|||||||
if (isValid) {
|
if (isValid) {
|
||||||
m_sci->markerDelete(m_editState.line, M_ERR);
|
m_sci->markerDelete(m_editState.line, M_ERR);
|
||||||
if (isSelected) m_sci->markerAdd(m_editState.line, M_SELECTED);
|
if (isSelected) m_sci->markerAdd(m_editState.line, M_SELECTED);
|
||||||
if (stateChanged) {
|
if (stateChanged)
|
||||||
if (m_editState.target == EditTarget::BaseAddress)
|
setEditComment("Enter=Save Esc=Cancel");
|
||||||
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
|
|
||||||
else
|
|
||||||
setEditComment("Enter=Save Esc=Cancel");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (isSelected) m_sci->markerDelete(m_editState.line, M_SELECTED);
|
if (isSelected) m_sci->markerDelete(m_editState.line, M_SELECTED);
|
||||||
m_sci->markerAdd(m_editState.line, M_ERR);
|
m_sci->markerAdd(m_editState.line, M_ERR);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
#include "providerregistry.h"
|
||||||
#include "themes/theme.h"
|
#include "themes/theme.h"
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
@@ -12,11 +13,6 @@ class QsciLexerCPP;
|
|||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
struct SavedSourceDisplay {
|
|
||||||
QString text;
|
|
||||||
bool active = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RcxEditor : public QWidget {
|
class RcxEditor : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@@ -163,6 +159,7 @@ private:
|
|||||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||||
QWidget* m_disasmPopup = nullptr; // TitleBodyPopup (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_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_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
||||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||||
const NodeTree* m_disasmTree = nullptr;
|
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.value,
|
||||||
field->data.LF_ENUMERATE.lfEasy.kind);
|
field->data.LF_ENUMERATE.lfEasy.kind);
|
||||||
if (eName)
|
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 += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
|
||||||
i += strnlen(eName, maxSize - i - 1) + 1;
|
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.name = name;
|
||||||
n.parentId = parentId;
|
n.parentId = parentId;
|
||||||
n.offset = offset;
|
n.offset = offset;
|
||||||
n.bitfieldMembers.append({name, bitPos, bitLen});
|
n.bitfieldMembers.push_back(BitfieldMember{name, bitPos, bitLen});
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
break;
|
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 ──
|
// ── Public API: enumeratePdbTypes ──
|
||||||
|
|
||||||
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg) {
|
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg) {
|
||||||
@@ -1126,6 +1238,11 @@ NodeTree importPdb(const QString& pdbPath, const QString& structFilter, QString*
|
|||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
|
PdbSymbolResult extractPdbSymbols(const QString&, QString* errorMsg) {
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
QVector<PdbTypeInfo> enumeratePdbTypes(const QString&, QString* errorMsg) {
|
QVector<PdbTypeInfo> enumeratePdbTypes(const QString&, QString* errorMsg) {
|
||||||
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
|
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -5,6 +5,25 @@
|
|||||||
|
|
||||||
namespace rcx {
|
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 {
|
struct PdbTypeInfo {
|
||||||
uint32_t typeIndex; // TPI type index
|
uint32_t typeIndex; // TPI type index
|
||||||
QString name; // struct/class/union/enum name
|
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
|
// Defer ref resolution if array references a class
|
||||||
if (!arrayClassName.isEmpty()) {
|
if (!arrayClassName.isEmpty()) {
|
||||||
pendingRefs.append({arrId, arrayClassName});
|
pendingRefs.push_back(PendingRef{arrId, arrayClassName});
|
||||||
}
|
}
|
||||||
|
|
||||||
childOffset += nodeSize > 0 ? nodeSize : 0;
|
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
|
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
|
||||||
int nodeIdx = tree.addNode(n);
|
int nodeIdx = tree.addNode(n);
|
||||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||||
pendingRefs.append({nodeId, ptrClass});
|
pendingRefs.push_back(PendingRef{nodeId, ptrClass});
|
||||||
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -335,7 +335,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointe
|
|||||||
if (!n.structTypeName.isEmpty()) {
|
if (!n.structTypeName.isEmpty()) {
|
||||||
int nodeIdx = tree.addNode(n);
|
int nodeIdx = tree.addNode(n);
|
||||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||||
pendingRefs.append({nodeId, n.structTypeName});
|
pendingRefs.push_back(PendingRef{nodeId, n.structTypeName});
|
||||||
} else {
|
} else {
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ struct Tokenizer {
|
|||||||
case '=': tk = TokKind::Equals; break;
|
case '=': tk = TokKind::Equals; break;
|
||||||
default: tk = TokKind::Other; break;
|
default: tk = TokKind::Other; break;
|
||||||
}
|
}
|
||||||
tokens.append({tk, QString(c), line});
|
tokens.push_back(Token{tk, QString(c), line});
|
||||||
pos++;
|
pos++;
|
||||||
}
|
}
|
||||||
tokens.append({TokKind::Eof, {}, line});
|
tokens.push_back(Token{TokKind::Eof, {}, line});
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -241,7 +241,7 @@ private:
|
|||||||
bool ok;
|
bool ok;
|
||||||
int val = m.captured(1).toInt(&ok, 16);
|
int val = m.captured(1).toInt(&ok, 16);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
offsets.append({commentLine, val});
|
offsets.push_back(LineOffset{commentLine, val});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ private:
|
|||||||
void parseIdent() {
|
void parseIdent() {
|
||||||
int start = pos;
|
int start = pos;
|
||||||
while (pos < src.size() && (src[pos].isLetterOrNumber() || src[pos] == '_')) 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() {
|
void parseNumber() {
|
||||||
@@ -276,7 +276,7 @@ private:
|
|||||||
// Skip integer suffixes (U, L, LL, ULL, etc.)
|
// Skip integer suffixes (U, L, LL, ULL, etc.)
|
||||||
while (pos < src.size() && (src[pos] == 'u' || src[pos] == 'U' ||
|
while (pos < src.size() && (src[pos] == 'u' || src[pos] == 'U' ||
|
||||||
src[pos] == 'l' || src[pos] == 'L')) pos++;
|
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;
|
nextValue = memberValue + 1;
|
||||||
|
|
||||||
// Skip comma between members
|
// Skip comma between members
|
||||||
@@ -1312,7 +1312,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
|
|
||||||
if (!field.pointerTarget.isEmpty() &&
|
if (!field.pointerTarget.isEmpty() &&
|
||||||
field.pointerTarget != QStringLiteral("void")) {
|
field.pointerTarget != QStringLiteral("void")) {
|
||||||
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
ctx.pendingRefs.push_back(PendingRef{nodeId, field.pointerTarget});
|
||||||
}
|
}
|
||||||
|
|
||||||
computedOffset = fieldOffset + ctx.ptrSize;
|
computedOffset = fieldOffset + ctx.ptrSize;
|
||||||
@@ -1342,7 +1342,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
n.offset = fieldOffset;
|
n.offset = fieldOffset;
|
||||||
int nodeIdx = ctx.tree.addNode(n);
|
int nodeIdx = ctx.tree.addNode(n);
|
||||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
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;
|
computedOffset = fieldOffset + elemSize;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -1461,7 +1461,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
|
|
||||||
int nodeIdx = ctx.tree.addNode(n);
|
int nodeIdx = ctx.tree.addNode(n);
|
||||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
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)
|
if (elemSize > 0)
|
||||||
computedOffset = fieldOffset + totalElements * elemSize;
|
computedOffset = fieldOffset + totalElements * elemSize;
|
||||||
continue;
|
continue;
|
||||||
@@ -1477,7 +1477,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
|
|
||||||
int nodeIdx = ctx.tree.addNode(n);
|
int nodeIdx = ctx.tree.addNode(n);
|
||||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
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)
|
if (elemSize > 0)
|
||||||
computedOffset = fieldOffset + elemSize;
|
computedOffset = fieldOffset + elemSize;
|
||||||
continue;
|
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")))
|
#define RCX_PLUGIN_EXPORT __attribute__((visibility("default")))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declarations
|
||||||
namespace rcx { class Provider; }
|
namespace rcx { class Provider; }
|
||||||
|
class QMenu;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin interface for Reclass
|
* Plugin interface for Reclass
|
||||||
@@ -129,6 +130,13 @@ public:
|
|||||||
* @return true if enumerateProcesses() should be called
|
* @return true if enumerateProcesses() should be called
|
||||||
*/
|
*/
|
||||||
virtual bool providesProcessList() const { return false; }
|
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
|
// 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 "scannerpanel.h"
|
||||||
#include "startpage.h"
|
#include "startpage.h"
|
||||||
#include "workspace_model.h"
|
#include "workspace_model.h"
|
||||||
|
namespace rcx { class SymbolDownloader; }
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
@@ -199,6 +200,28 @@ private:
|
|||||||
DockGripWidget* m_scanDockGrip = nullptr;
|
DockGripWidget* m_scanDockGrip = nullptr;
|
||||||
void createScannerDock();
|
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
|
// Start page
|
||||||
StartPageWidget* m_startPage = nullptr;
|
StartPageWidget* m_startPage = nullptr;
|
||||||
Q_INVOKABLE void showStartPage();
|
Q_INVOKABLE void showStartPage();
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ void McpBridge::onNewConnection() {
|
|||||||
auto* pending = m_server->nextPendingConnection();
|
auto* pending = m_server->nextPendingConnection();
|
||||||
if (!pending) return;
|
if (!pending) return;
|
||||||
|
|
||||||
m_clients.append({pending, {}, false});
|
m_clients.push_back(ClientState{pending, {}, false});
|
||||||
|
|
||||||
connect(pending, &QLocalSocket::readyRead,
|
connect(pending, &QLocalSocket::readyRead,
|
||||||
this, &McpBridge::onReadyRead);
|
this, &McpBridge::onReadyRead);
|
||||||
@@ -156,7 +156,7 @@ void McpBridge::onReadyRead() {
|
|||||||
if (line.isEmpty()) continue;
|
if (line.isEmpty()) continue;
|
||||||
|
|
||||||
if (m_processing) {
|
if (m_processing) {
|
||||||
m_pendingRequests.append({sock, line});
|
m_pendingRequests.push_back(PendingRequest{sock, line});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
m_processing = true;
|
m_processing = true;
|
||||||
@@ -819,7 +819,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
QJsonArray nodeArr;
|
QJsonArray nodeArr;
|
||||||
struct QueueEntry { uint64_t parentId; int depth; };
|
struct QueueEntry { uint64_t parentId; int depth; };
|
||||||
QVector<QueueEntry> queue;
|
QVector<QueueEntry> queue;
|
||||||
queue.append({filterParentId, 0});
|
queue.push_back(QueueEntry{filterParentId, 0});
|
||||||
|
|
||||||
int totalCount = 0; // total nodes that match depth filter
|
int totalCount = 0; // total nodes that match depth filter
|
||||||
int emitted = 0;
|
int emitted = 0;
|
||||||
@@ -839,13 +839,13 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
if (totalCount <= offset) {
|
if (totalCount <= offset) {
|
||||||
// Still skipping — but enqueue children for counting
|
// Still skipping — but enqueue children for counting
|
||||||
if (entry.depth + 1 <= maxDepth)
|
if (entry.depth + 1 <= maxDepth)
|
||||||
queue.append({n.id, entry.depth + 1});
|
queue.push_back(QueueEntry{n.id, entry.depth + 1});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (emitted >= limit) {
|
if (emitted >= limit) {
|
||||||
// Past limit — just keep counting total
|
// Past limit — just keep counting total
|
||||||
if (entry.depth + 1 <= maxDepth)
|
if (entry.depth + 1 <= maxDepth)
|
||||||
queue.append({n.id, entry.depth + 1});
|
queue.push_back(QueueEntry{n.id, entry.depth + 1});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,7 +875,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
|
|
||||||
// Enqueue children if we haven't hit depth limit
|
// Enqueue children if we haven't hit depth limit
|
||||||
if (entry.depth + 1 <= maxDepth)
|
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);
|
if (errOut) *errOut = QStringLiteral("regions[%1]: end must be > start").arg(i);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
out.append({start, end});
|
out.push_back(AddressRange{start, end});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ void PluginManager::LoadPlugins()
|
|||||||
|
|
||||||
for (const QFileInfo& fileInfo : files)
|
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());
|
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();
|
qDebug() << "PluginManager: Loaded plugin:" << plugin->Name().c_str() << plugin->Version().c_str() << "by" << plugin->Author().c_str();
|
||||||
|
|
||||||
// Store plugin entry
|
// Store plugin entry
|
||||||
m_entries.append({library, plugin});
|
m_entries.push_back(PluginEntry{library, plugin});
|
||||||
m_plugins.append(plugin);
|
m_plugins.append(plugin);
|
||||||
|
|
||||||
// Auto-register providers in global registry
|
// Auto-register providers in global registry
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "providerregistry.h"
|
#include "providerregistry.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
ProviderRegistry& ProviderRegistry::instance() {
|
ProviderRegistry& ProviderRegistry::instance() {
|
||||||
static ProviderRegistry s_instance;
|
static ProviderRegistry s_instance;
|
||||||
@@ -56,3 +59,57 @@ const ProviderRegistry::ProviderInfo* ProviderRegistry::findProvider(const QStri
|
|||||||
void ProviderRegistry::clear() {
|
void ProviderRegistry::clear() {
|
||||||
m_providers.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
|
// Forward declarations
|
||||||
namespace rcx { class Provider; }
|
namespace rcx { class Provider; }
|
||||||
class QWidget;
|
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
|
* Global registry for data source providers
|
||||||
@@ -56,7 +63,13 @@ public:
|
|||||||
|
|
||||||
// Clear all providers
|
// Clear all providers
|
||||||
void clear();
|
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:
|
private:
|
||||||
ProviderRegistry() = default;
|
ProviderRegistry() = default;
|
||||||
QList<ProviderInfo> m_providers;
|
QList<ProviderInfo> m_providers;
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ struct MemoryRegion {
|
|||||||
QString moduleName;
|
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 {
|
class Provider {
|
||||||
public:
|
public:
|
||||||
virtual ~Provider() = default;
|
virtual ~Provider() = default;
|
||||||
@@ -80,6 +87,22 @@ public:
|
|||||||
struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; };
|
struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; };
|
||||||
virtual QVector<ThreadInfo> tebs() const { return {}; }
|
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) ---
|
// --- Derived convenience (non-virtual, never override) ---
|
||||||
|
|
||||||
bool isValid() const { return size() > 0; }
|
bool isValid() const { return size() > 0; }
|
||||||
|
|||||||
342
src/rcxtooltip.h
342
src/rcxtooltip.h
@@ -1,241 +1,173 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "themes/thememanager.h"
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QLabel>
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QApplication>
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QTimer>
|
#include <QApplication>
|
||||||
#include <QPropertyAnimation>
|
#include <QMouseEvent>
|
||||||
#include <QCursor>
|
#include <functional>
|
||||||
#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)
|
|
||||||
|
|
||||||
namespace rcx {
|
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 {
|
class RcxTooltip : public QWidget {
|
||||||
public:
|
public:
|
||||||
static RcxTooltip* instance() {
|
static constexpr int kArrowH = 8;
|
||||||
static RcxTooltip* s = nullptr;
|
static constexpr int kArrowW = 14;
|
||||||
if (!s) {
|
static constexpr int kRadius = 6;
|
||||||
s = new RcxTooltip;
|
static constexpr int kPad = 10;
|
||||||
QObject::connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
static constexpr int kGap = 4;
|
||||||
s, [](const rcx::Theme&) { /* colors read live in paintEvent */ });
|
static constexpr int kMaxW = 550;
|
||||||
}
|
|
||||||
return s;
|
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) {
|
void setTheme(const QColor& bg, const QColor& border,
|
||||||
if (!trigger || text.isEmpty()) {
|
const QColor& title, const QColor& body, const QColor& sep) {
|
||||||
TIP_LOG("[TIP] showFor: null trigger or empty text -- dismiss\n");
|
m_bg = bg; m_border = border;
|
||||||
dismiss(); return;
|
m_titleCol = title; m_bodyCol = body; m_sepCol = sep;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same widget+text already showing — do nothing (prevents teleport)
|
void populate(const QString& title, const QString& body, const QFont& font) {
|
||||||
if (m_trigger == trigger && m_text == text && isVisible()) {
|
if (title == m_title && body == m_body && isVisible()) return;
|
||||||
TIP_LOG("[TIP] showFor: same widget+text, already visible -- skip\n");
|
m_title = title; m_body = body;
|
||||||
return;
|
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",
|
// `anchor`: global screen point where the arrow tip touches.
|
||||||
qPrintable(text), (void*)trigger, trigger->metaObject()->className());
|
// Typically the center-bottom of the hovered span.
|
||||||
|
void showAt(const QPoint& anchor) {
|
||||||
// Cancel pending dismiss
|
QRect scr = screenAt(anchor);
|
||||||
if (m_dismissTimer) m_dismissTimer->stop();
|
int w = m_bw, h = m_bh + kArrowH;
|
||||||
|
m_up = (anchor.y() + h <= scr.bottom());
|
||||||
m_trigger = trigger;
|
int x = qBound(scr.left() + 2, anchor.x() - w / 2, scr.right() - w - 2);
|
||||||
m_text = text;
|
int y = m_up ? anchor.y() : anchor.y() - h;
|
||||||
|
m_ax = qBound(kRadius + kArrowW/2 + 1, anchor.x() - x,
|
||||||
m_label->setText(text);
|
w - kRadius - kArrowW/2 - 1);
|
||||||
m_label->adjustSize();
|
setFixedSize(w, h);
|
||||||
|
|
||||||
// ── 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);
|
|
||||||
move(x, y);
|
move(x, y);
|
||||||
|
if (!isVisible()) show();
|
||||||
if (!isVisible()) {
|
update();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void dismiss() {
|
void dismiss() { if (isVisible()) hide(); }
|
||||||
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;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent*) override {
|
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);
|
QPainter p(this);
|
||||||
p.setRenderHint(QPainter::Antialiasing);
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
// Fill entire widget with the tooltip background first
|
// Body rect (excludes arrow space)
|
||||||
// (no WA_TranslucentBackground, so unpainted areas would be opaque garbage)
|
QRectF b(0.5, m_up ? kArrowH + 0.5 : 0.5,
|
||||||
p.fillRect(rect(), theme.backgroundAlt);
|
width() - 1.0, m_bh - 1.0);
|
||||||
|
qreal r = kRadius, ax = m_ax, ah = kArrowW / 2.0;
|
||||||
|
|
||||||
// Build path: rounded body + triangle arrow
|
// ── Single contiguous path: rounded rect + arrow notch ──
|
||||||
QPainterPath path;
|
// No QPainterPath::united() — that causes junction artifacts.
|
||||||
path.addRoundedRect(QRectF(m_bodyRect), 4.0, 4.0);
|
// Clockwise from top-left, inserting the arrow inline.
|
||||||
|
QPainterPath pp;
|
||||||
// Triangle arrow
|
pp.moveTo(b.left() + r, b.top());
|
||||||
QPolygonF arrow;
|
if (m_up) {
|
||||||
if (m_arrowDown) {
|
pp.lineTo(ax - ah, b.top());
|
||||||
int ay = m_bodyRect.bottom();
|
pp.lineTo(ax, 0.5);
|
||||||
arrow << QPointF(m_arrowLocalX - kArrowHalfW, ay)
|
pp.lineTo(ax + ah, b.top());
|
||||||
<< 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);
|
|
||||||
}
|
}
|
||||||
QPainterPath arrowPath;
|
pp.lineTo(b.right() - r, b.top());
|
||||||
arrowPath.addPolygon(arrow);
|
pp.arcTo(b.right() - 2*r, b.top(), 2*r, 2*r, 90, -90);
|
||||||
arrowPath.closeSubpath();
|
pp.lineTo(b.right(), b.bottom() - r);
|
||||||
path = path.united(arrowPath);
|
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(m_border, 1));
|
||||||
p.setPen(QPen(theme.border, 1.0));
|
p.setBrush(m_bg);
|
||||||
p.setBrush(theme.backgroundAlt);
|
p.drawPath(pp);
|
||||||
p.drawPath(path);
|
|
||||||
|
// ── 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:
|
private:
|
||||||
explicit RcxTooltip()
|
static QRect screenAt(const QPoint& pt) {
|
||||||
: QWidget(nullptr, Qt::ToolTip | Qt::FramelessWindowHint)
|
auto* s = QApplication::screenAt(pt);
|
||||||
{
|
return s ? s->availableGeometry() : QRect(0, 0, 1920, 1080);
|
||||||
// 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(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateLabelStyle() {
|
void recalc() {
|
||||||
const auto& theme = ThemeManager::instance().current();
|
QFontMetrics tf(m_bold), bf(m_font);
|
||||||
m_label->setStyleSheet(
|
int maxW = m_title.isEmpty() ? 0 : tf.horizontalAdvance(m_title);
|
||||||
QStringLiteral("QLabel { color: %1; background: transparent; padding: 0; }")
|
for (const auto& l : m_lines) maxW = qMax(maxW, bf.horizontalAdvance(l));
|
||||||
.arg(theme.text.name()));
|
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;
|
QString m_title, m_body;
|
||||||
QWidget* m_trigger = nullptr;
|
QStringList m_lines;
|
||||||
QString m_text;
|
QFont m_font, m_bold;
|
||||||
QTimer* m_dismissTimer = nullptr;
|
QColor m_bg{30, 30, 30}, m_border{60, 60, 60};
|
||||||
bool m_arrowDown = true;
|
QColor m_titleCol{220, 220, 220}, m_bodyCol{180, 180, 180}, m_sepCol{60, 60, 60};
|
||||||
int m_arrowLocalX = 0;
|
bool m_up = true;
|
||||||
QRect m_bodyRect;
|
int m_ax = 0, m_bw = 0, m_bh = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<file alias="file-binary.svg">vsicons/file-binary.svg</file>
|
<file alias="file-binary.svg">vsicons/file-binary.svg</file>
|
||||||
<file alias="debug.svg">vsicons/debug.svg</file>
|
<file alias="debug.svg">vsicons/debug.svg</file>
|
||||||
<file alias="close.svg">vsicons/close.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-left.svg">vsicons/arrow-left.svg</file>
|
||||||
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
|
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
|
||||||
<file alias="split-horizontal.svg">vsicons/split-horizontal.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-enum.svg">vsicons/symbol-enum.svg</file>
|
||||||
<file alias="symbol-class.svg">vsicons/symbol-class.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-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="server-process.svg">vsicons/server-process.svg</file>
|
||||||
<file alias="remote.svg">vsicons/remote.svg</file>
|
<file alias="remote.svg">vsicons/remote.svg</file>
|
||||||
<file alias="plug.svg">vsicons/plug.svg</file>
|
<file alias="plug.svg">vsicons/plug.svg</file>
|
||||||
|
|||||||
@@ -171,8 +171,8 @@ private:
|
|||||||
for (const auto& path : s.value("recentFiles").toStringList()) {
|
for (const auto& path : s.value("recentFiles").toStringList()) {
|
||||||
QFileInfo fi(path);
|
QFileInfo fi(path);
|
||||||
if (!fi.exists()) continue;
|
if (!fi.exists()) continue;
|
||||||
m_all.append({fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
|
m_all.push_back(Entry{fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
|
||||||
fi.lastModified(), false});
|
fi.lastModified(), false});
|
||||||
}
|
}
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
|
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
|
||||||
@@ -180,8 +180,8 @@ private:
|
|||||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||||
#endif
|
#endif
|
||||||
for (const auto& fn : exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name))
|
for (const auto& fn : exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name))
|
||||||
m_all.append({exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
|
m_all.push_back(Entry{exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
|
||||||
QFileInfo(exDir.filePath(fn)).lastModified(), true});
|
QFileInfo(exDir.filePath(fn)).lastModified(), true});
|
||||||
}
|
}
|
||||||
|
|
||||||
void buildGroups() {
|
void buildGroups() {
|
||||||
@@ -207,7 +207,7 @@ private:
|
|||||||
static const char* names[] = {"Today","Yesterday","This week","This month","Older","Examples"};
|
static const char* names[] = {"Today","Yesterday","This week","This month","Older","Examples"};
|
||||||
m_groups.clear();
|
m_groups.clear();
|
||||||
for (int i = 0; i < 6; i++)
|
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;
|
m_scrollY = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,13 +223,11 @@ private:
|
|||||||
{":/vsicons/debug.svg", "Import PDB", "Import types from a .pdb symbol file"}
|
{":/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
|
// Sharp-cornered panel background
|
||||||
QPainterPath clip;
|
|
||||||
clip.addRoundedRect(QRectF(x, y, w, panelH), R, R);
|
|
||||||
p.save();
|
p.save();
|
||||||
p.setClipPath(clip);
|
p.setClipRect(QRectF(x, y, w, panelH));
|
||||||
p.fillRect(x, y, w, panelH, m_t.background);
|
p.fillRect(x, y, w, panelH, m_t.background);
|
||||||
|
|
||||||
for (int i = 0; i < N; i++) {
|
for (int i = 0; i < N; i++) {
|
||||||
@@ -289,7 +287,7 @@ private:
|
|||||||
if (gi > 0) fy += 15;
|
if (gi > 0) fy += 15;
|
||||||
|
|
||||||
// Group header
|
// 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);
|
p.setPen(Qt::NoPen); p.setBrush(m_t.text);
|
||||||
int triX = x + 8, triY = fy + 11;
|
int triX = x + 8, triY = fy + 11;
|
||||||
QPolygonF tri;
|
QPolygonF tri;
|
||||||
@@ -307,7 +305,7 @@ private:
|
|||||||
for (int ei : g.entries) {
|
for (int ei : g.entries) {
|
||||||
auto& e = m_filtered[ei];
|
auto& e = m_filtered[ei];
|
||||||
QRectF er(x, fy, w, 52);
|
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);
|
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",
|
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 <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
|
#include <QTimer>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
@@ -25,11 +26,23 @@ TitleBarWidget::TitleBarWidget(QWidget* parent)
|
|||||||
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
layout->addWidget(m_appLabel);
|
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 = new QMenuBar(this);
|
||||||
m_menuBar->setNativeMenuBar(false);
|
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);
|
m_menuBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||||
layout->addWidget(m_menuBar);
|
layout->addWidget(m_menuBar);
|
||||||
|
#endif
|
||||||
|
|
||||||
layout->addStretch();
|
layout->addStretch();
|
||||||
|
|
||||||
@@ -116,6 +129,17 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
|||||||
m_btnMin->setStyleSheet(btnStyle);
|
m_btnMin->setStyleSheet(btnStyle);
|
||||||
m_btnMax->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
|
// Close button: themed red hover
|
||||||
m_btnClose->setStyleSheet(QStringLiteral(
|
m_btnClose->setStyleSheet(QStringLiteral(
|
||||||
"QToolButton { background: transparent; border: none; }"
|
"QToolButton { background: transparent; border: none; }"
|
||||||
@@ -164,6 +188,58 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
|||||||
action->setText("&" + result);
|
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() {
|
void TitleBarWidget::updateMaximizeIcon() {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public:
|
|||||||
void setShowIcon(bool show);
|
void setShowIcon(bool show);
|
||||||
void setMenuBarTitleCase(bool titleCase);
|
void setMenuBarTitleCase(bool titleCase);
|
||||||
bool menuBarTitleCase() const { return m_titleCase; }
|
bool menuBarTitleCase() const { return m_titleCase; }
|
||||||
|
void finalizeMenuBar();
|
||||||
|
|
||||||
void updateMaximizeIcon();
|
void updateMaximizeIcon();
|
||||||
|
|
||||||
@@ -25,16 +26,20 @@ protected:
|
|||||||
void mousePressEvent(QMouseEvent* event) override;
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLabel* m_appLabel = nullptr;
|
QLabel* m_appLabel = nullptr;
|
||||||
QMenuBar* m_menuBar = nullptr;
|
QMenuBar* m_menuBar = nullptr;
|
||||||
|
QHBoxLayout* m_menuBtnLayout = nullptr;
|
||||||
|
QVector<QToolButton*> m_menuButtons;
|
||||||
QToolButton* m_btnMin = nullptr;
|
QToolButton* m_btnMin = nullptr;
|
||||||
QToolButton* m_btnMax = nullptr;
|
QToolButton* m_btnMax = nullptr;
|
||||||
QToolButton* m_btnClose = nullptr;
|
QToolButton* m_btnClose = nullptr;
|
||||||
|
|
||||||
Theme m_theme;
|
Theme m_theme;
|
||||||
bool m_titleCase = false;
|
bool m_titleCase = false;
|
||||||
|
bool m_useToolButtons = false;
|
||||||
|
|
||||||
QToolButton* makeChromeButton(const QString& iconPath);
|
QToolButton* makeChromeButton(const QString& iconPath);
|
||||||
void toggleMaximize();
|
void toggleMaximize();
|
||||||
|
|||||||
@@ -191,23 +191,26 @@ inline FeatureResult countFlagFeatures(uint32_t val,
|
|||||||
// ── Pointer feature checker ──
|
// ── Pointer feature checker ──
|
||||||
|
|
||||||
inline FeatureResult countPtrFeatures64(uint64_t val) {
|
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)
|
if (val == 0 || val == 0xFFFFFFFFFFFFFFFFULL || val == 0x00000000FFFFFFFFULL)
|
||||||
return {0, 6};
|
return {0, 5};
|
||||||
|
|
||||||
int passed = 0, checked = 6;
|
// Hard reject: non-canonical address — impossible to dereference on x64
|
||||||
// Feature 1: canonical 48-bit address (sign-extended from bit 47)
|
// User-mode: 0x0000000000000000 – 0x00007FFFFFFFFFFF
|
||||||
passed += (val <= 0x00007FFFFFFFFFFFULL
|
// Kernel: 0xFFFF800000000000 – 0xFFFFFFFFFFFFFFFF
|
||||||
|| val >= 0xFFFF800000000000ULL) ? 1 : 0;
|
if (val > 0x00007FFFFFFFFFFFULL && val < 0xFFFF800000000000ULL)
|
||||||
// Feature 2: aligned to 8 (heap/vtable allocations)
|
return {0, 5};
|
||||||
|
|
||||||
|
int passed = 0, checked = 5;
|
||||||
|
// Feature 1: aligned to 8 (heap/vtable allocations)
|
||||||
passed += ((val & 7) == 0) ? 1 : 0;
|
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;
|
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;
|
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;
|
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;
|
passed += (val < 0xFFFF800000000000ULL) ? 1 : 0;
|
||||||
return {passed, checked};
|
return {passed, checked};
|
||||||
}
|
}
|
||||||
@@ -289,13 +292,13 @@ struct Candidate {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inline void addCandidate(QVector<Candidate>& out, NodeKind k, int score) {
|
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) {
|
inline void addSplitCandidate(QVector<Candidate>& out, NodeKind k, int count, int score) {
|
||||||
if (score >= 25) {
|
if (score >= 25) {
|
||||||
QVector<NodeKind> kinds(count, k);
|
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)
|
if (h.ptrSize == 8)
|
||||||
addCandidate(out, NodeKind::Pointer64, featureScore(countPtrFeatures64(u64)));
|
addCandidate(out, NodeKind::Pointer64, featureScore(countPtrFeatures64(u64)));
|
||||||
|
|
||||||
// Double
|
// Double — rare in RE work; require strong evidence
|
||||||
{
|
{
|
||||||
double d; std::memcpy(&d, data, 8);
|
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);
|
double ad = std::fabs(d);
|
||||||
passed += (d == 0.0 || (ad >= 1e-6 && ad <= 1e12)) ? 1 : 0;
|
uint64_t mantissa = u64 & 0x000FFFFFFFFFFFFFull;
|
||||||
addCandidate(out, NodeKind::Double, featureScore({passed, checked}));
|
// 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
|
// UTF8
|
||||||
addCandidate(out, NodeKind::UTF8, featureScore(countStringFeatures(data, 8)));
|
addCandidate(out, NodeKind::UTF8, featureScore(countStringFeatures(data, 8)));
|
||||||
|
|
||||||
// UInt64 / Int64
|
// UInt64 / Int64 — only meaningful when value exceeds 32-bit range
|
||||||
{
|
if ((u64 >> 32) != 0) {
|
||||||
int passed = 0, checked = 4;
|
int passed = 0, checked = 3;
|
||||||
// Feature 1: fits in 32 bits (small constant, not an address)
|
// Feature 1: non-zero (always true after guard)
|
||||||
passed += (u64 <= 0xFFFFFFFFull) ? 1 : 0;
|
passed += 1;
|
||||||
// Feature 2: upper 32 bits are zero (confirms it's a small value, not a pointer)
|
// Feature 2: reasonable magnitude (below kernel range)
|
||||||
passed += ((u64 >> 32) == 0) ? 1 : 0;
|
passed += (u64 < 0x0000FFFFFFFFFFFFULL) ? 1 : 0;
|
||||||
// Feature 3: non-zero
|
// Feature 3: monotonic or page-aligned
|
||||||
passed += (u64 != 0) ? 1 : 0;
|
passed += (h.monotonic || (u64 & 0xFFF) == 0) ? 1 : 0;
|
||||||
// Feature 4: monotonic or very small (< 0x10000)
|
|
||||||
passed += (h.monotonic || u64 < 0x10000) ? 1 : 0;
|
|
||||||
addCandidate(out, NodeKind::UInt64, featureScore({passed, checked}));
|
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) {
|
for (const auto& c : deduped) {
|
||||||
int str = strengthFromScore(c.score);
|
int str = strengthFromScore(c.score);
|
||||||
if (str > 0)
|
if (str > 0)
|
||||||
result.append({c.kinds, c.score, str});
|
result.push_back(TypeSuggestion{c.kinds, c.score, str});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1030,7 +1030,7 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
|||||||
else if (t.category == TypeEntry::CatEnum) enumCount++;
|
else if (t.category == TypeEntry::CatEnum) enumCount++;
|
||||||
else typeCount++;
|
else typeCount++;
|
||||||
if (catAllowed(t))
|
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(),
|
std::sort(scored.begin(), scored.end(),
|
||||||
[](const Scored& a, const Scored& b) { return a.score > b.score; });
|
[](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];
|
const Node& n = tab.tree->nodes[idx];
|
||||||
if (n.kind != NodeKind::Struct) continue;
|
if (n.kind != NodeKind::Struct) continue;
|
||||||
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||||
enums.append({&n, tab.subPtr, tab.tree});
|
enums.push_back(Entry{&n, tab.subPtr, tab.tree});
|
||||||
else
|
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];
|
const Node& n = tab.tree->nodes[idx];
|
||||||
if (n.kind != NodeKind::Struct) continue;
|
if (n.kind != NodeKind::Struct) continue;
|
||||||
bool ie = n.resolvedClassKeyword() == QStringLiteral("enum");
|
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
|
// Build TabInfo array
|
||||||
QVector<TabInfo> tabs;
|
QVector<TabInfo> tabs;
|
||||||
for (const auto& t : trees)
|
for (const auto& t : trees)
|
||||||
tabs.append({ &t, QStringLiteral("test"), nullptr });
|
tabs.push_back(TabInfo{ &t, QStringLiteral("test"), nullptr });
|
||||||
|
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
const int ITERS = 20;
|
const int ITERS = 20;
|
||||||
@@ -244,7 +244,7 @@ void BenchProject::benchWorkspaceSearch()
|
|||||||
|
|
||||||
QVector<TabInfo> tabs;
|
QVector<TabInfo> tabs;
|
||||||
for (const auto& t : trees)
|
for (const auto& t : trees)
|
||||||
tabs.append({ &t, QStringLiteral("test"), nullptr });
|
tabs.push_back(TabInfo{ &t, QStringLiteral("test"), nullptr });
|
||||||
|
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
buildProjectExplorer(&model, tabs);
|
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);
|
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 --
|
// -- Validate with new syntax --
|
||||||
|
|
||||||
void validateIdentifier() {
|
void validateIdentifier() {
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ private slots:
|
|||||||
// Only include the pointer-expanded ones (near vtable at 0x100)
|
// Only include the pointer-expanded ones (near vtable at 0x100)
|
||||||
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
|
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
|
||||||
int nodeIdx = lm.nodeIdx;
|
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()});
|
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;
|
if (!m_server->listen(name)) return false;
|
||||||
connect(m_server, &QLocalServer::newConnection, this, [this]() {
|
connect(m_server, &QLocalServer::newConnection, this, [this]() {
|
||||||
while (auto* s = m_server->nextPendingConnection()) {
|
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::readyRead, this, [this, s]() { processSocket(s); });
|
||||||
connect(s, &QLocalSocket::disconnected, this, [this, s]() {
|
connect(s, &QLocalSocket::disconnected, this, [this, s]() {
|
||||||
for (int i = 0; i < m_clients.size(); i++)
|
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)
|
data[16] = 0xAA; // in region 1 (executable)
|
||||||
|
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0, 16, true, true, false, "heap"});
|
regions.push_back(MemoryRegion{0, 16, true, true, false, "heap"});
|
||||||
regions.append({16, 16, true, false, true, "code"});
|
regions.push_back(MemoryRegion{16, 16, true, false, true, "code"});
|
||||||
|
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
@@ -671,8 +671,8 @@ private slots:
|
|||||||
data[16] = 0xBB; // region 1 (not writable)
|
data[16] = 0xBB; // region 1 (not writable)
|
||||||
|
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0, 16, true, true, false, "data"});
|
regions.push_back(MemoryRegion{0, 16, true, true, false, "data"});
|
||||||
regions.append({16, 16, true, false, true, "code"});
|
regions.push_back(MemoryRegion{16, 16, true, false, true, "code"});
|
||||||
|
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
@@ -698,9 +698,9 @@ private slots:
|
|||||||
data[32] = 0xCC; // region 2: +w +x
|
data[32] = 0xCC; // region 2: +w +x
|
||||||
|
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0, 16, true, true, false, "data"});
|
regions.push_back(MemoryRegion{0, 16, true, true, false, "data"});
|
||||||
regions.append({16, 16, true, false, true, "code"});
|
regions.push_back(MemoryRegion{16, 16, true, false, true, "code"});
|
||||||
regions.append({32, 16, true, true, true, "rwx"});
|
regions.push_back(MemoryRegion{32, 16, true, true, true, "rwx"});
|
||||||
|
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
@@ -726,7 +726,7 @@ private slots:
|
|||||||
data[0] = 0xDD;
|
data[0] = 0xDD;
|
||||||
|
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
@@ -943,8 +943,8 @@ private slots:
|
|||||||
|
|
||||||
void provider_customRegions() {
|
void provider_customRegions() {
|
||||||
QVector<MemoryRegion> regs;
|
QVector<MemoryRegion> regs;
|
||||||
regs.append({0x1000, 0x2000, true, true, false, "heap"});
|
regs.push_back(MemoryRegion{0x1000, 0x2000, true, true, false, "heap"});
|
||||||
regs.append({0x3000, 0x1000, true, false, true, "code"});
|
regs.push_back(MemoryRegion{0x3000, 0x1000, true, false, true, "code"});
|
||||||
|
|
||||||
RegionProvider p(QByteArray(0x4000, '\0'), regs);
|
RegionProvider p(QByteArray(0x4000, '\0'), regs);
|
||||||
auto result = p.enumerateRegions();
|
auto result = p.enumerateRegions();
|
||||||
@@ -982,9 +982,9 @@ private slots:
|
|||||||
data[36] = 0xEE; // region 2
|
data[36] = 0xEE; // region 2
|
||||||
|
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0, 16, true, true, false, "region0"});
|
regions.push_back(MemoryRegion{0, 16, true, true, false, "region0"});
|
||||||
regions.append({16, 16, true, true, false, "region1"});
|
regions.push_back(MemoryRegion{16, 16, true, true, false, "region1"});
|
||||||
regions.append({32, 16, true, true, false, "region2"});
|
regions.push_back(MemoryRegion{32, 16, true, true, false, "region2"});
|
||||||
|
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
@@ -1215,7 +1215,7 @@ private slots:
|
|||||||
data[160] = char(0xCC);
|
data[160] = char(0xCC);
|
||||||
data[210] = char(0xCC);
|
data[210] = char(0xCC);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1233,7 +1233,7 @@ private slots:
|
|||||||
void scan_constrainRegions_noOverlap() {
|
void scan_constrainRegions_noOverlap() {
|
||||||
QByteArray data(32, char(0xEE));
|
QByteArray data(32, char(0xEE));
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1256,8 +1256,8 @@ private slots:
|
|||||||
data[10] = char(0xDD);
|
data[10] = char(0xDD);
|
||||||
data[35] = char(0xDD);
|
data[35] = char(0xDD);
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0, 16, true, true, false, {}});
|
regions.push_back(MemoryRegion{0, 16, true, true, false, {}});
|
||||||
regions.append({32, 16, true, true, false, {}});
|
regions.push_back(MemoryRegion{32, 16, true, true, false, {}});
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1279,7 +1279,7 @@ private slots:
|
|||||||
data[120] = char(0xAB);
|
data[120] = char(0xAB);
|
||||||
data[160] = char(0xAB);
|
data[160] = char(0xAB);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1300,8 +1300,8 @@ private slots:
|
|||||||
data[0x1500] = char(0xCC);
|
data[0x1500] = char(0xCC);
|
||||||
data[0x5500] = char(0xCC);
|
data[0x5500] = char(0xCC);
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0x1000, 0x1000, true, false, true, QString("game.exe")});
|
regions.push_back(MemoryRegion{0x1000, 0x1000, true, false, true, QString("game.exe")});
|
||||||
regions.append({0x5000, 0x1000, true, true, false, {}});
|
regions.push_back(MemoryRegion{0x5000, 0x1000, true, true, false, {}});
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1345,8 +1345,8 @@ private slots:
|
|||||||
data[12] = char(0xEF);
|
data[12] = char(0xEF);
|
||||||
data[20] = char(0xEF);
|
data[20] = char(0xEF);
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0, 16, true, true, false, {}});
|
regions.push_back(MemoryRegion{0, 16, true, true, false, {}});
|
||||||
regions.append({16, 16, true, true, false, {}});
|
regions.push_back(MemoryRegion{16, 16, true, true, false, {}});
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1368,8 +1368,8 @@ private slots:
|
|||||||
data[0x1100] = char(0xBB);
|
data[0x1100] = char(0xBB);
|
||||||
data[0x2100] = char(0xBB);
|
data[0x2100] = char(0xBB);
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0x1000, 0x1000, true, false, true, {}});
|
regions.push_back(MemoryRegion{0x1000, 0x1000, true, false, true, {}});
|
||||||
regions.append({0x2000, 0x1000, true, true, false, {}});
|
regions.push_back(MemoryRegion{0x2000, 0x1000, true, true, false, {}});
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1394,7 +1394,7 @@ private slots:
|
|||||||
data[15] = char(0xAA); // inside region, should be found
|
data[15] = char(0xAA); // inside region, should be found
|
||||||
data[25] = char(0xAA); // outside region, should NOT be found
|
data[25] = char(0xAA); // outside region, should NOT be found
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1415,7 +1415,7 @@ private slots:
|
|||||||
data[5] = char(0xBB);
|
data[5] = char(0xBB);
|
||||||
data[15] = char(0xBB);
|
data[15] = char(0xBB);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1506,7 +1506,7 @@ private slots:
|
|||||||
QByteArray data(0x10000, 0);
|
QByteArray data(0x10000, 0);
|
||||||
data[0x8100] = char(0xFF);
|
data[0x8100] = char(0xFF);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1581,7 +1581,7 @@ private slots:
|
|||||||
QByteArray data(64, 0);
|
QByteArray data(64, 0);
|
||||||
data[20] = char(0xFE);
|
data[20] = char(0xFE);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1602,7 +1602,7 @@ private slots:
|
|||||||
QByteArray data(64, 0);
|
QByteArray data(64, 0);
|
||||||
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
|
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1624,7 +1624,7 @@ private slots:
|
|||||||
QByteArray data(64, 0);
|
QByteArray data(64, 0);
|
||||||
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
|
data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
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.
|
// Region [0, 64). Constraint [30, 32). 4-byte pattern can't fit in 2 bytes.
|
||||||
QByteArray data(64, char(0xAA));
|
QByteArray data(64, char(0xAA));
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1663,7 +1663,7 @@ private slots:
|
|||||||
QByteArray data(64, 0);
|
QByteArray data(64, 0);
|
||||||
data[30] = char(0x11); data[31] = char(0x22); data[32] = char(0x33); data[33] = char(0x44);
|
data[30] = char(0x11); data[31] = char(0x22); data[32] = char(0x33); data[33] = char(0x44);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1686,8 +1686,8 @@ private slots:
|
|||||||
data[15] = char(0x77); // last byte of first region
|
data[15] = char(0x77); // last byte of first region
|
||||||
data[16] = char(0x77); // first byte of second region
|
data[16] = char(0x77); // first byte of second region
|
||||||
QVector<MemoryRegion> regions;
|
QVector<MemoryRegion> regions;
|
||||||
regions.append({0, 16, true, true, false, {}});
|
regions.push_back(MemoryRegion{0, 16, true, true, false, {}});
|
||||||
regions.append({16, 16, true, true, false, {}});
|
regions.push_back(MemoryRegion{16, 16, true, true, false, {}});
|
||||||
auto prov = std::make_shared<RegionProvider>(data, regions);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
@@ -1711,7 +1711,7 @@ private slots:
|
|||||||
QByteArray data(64, 0);
|
QByteArray data(64, 0);
|
||||||
data[10] = char(0xAA); data[11] = char(0xBB); data[12] = char(0xCC); data[13] = char(0xDD);
|
data[10] = char(0xAA); data[11] = char(0xBB); data[12] = char(0xCC); data[13] = char(0xDD);
|
||||||
QVector<MemoryRegion> regions;
|
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);
|
auto prov = std::make_shared<RegionProvider>(data, regions);
|
||||||
ScanEngine engine;
|
ScanEngine engine;
|
||||||
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
QSignalSpy finSpy(&engine, &ScanEngine::finished);
|
||||||
|
|||||||
@@ -9,32 +9,35 @@
|
|||||||
using namespace rcx;
|
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:
|
// Validates:
|
||||||
// - Actual pixel rendering (catches WA_TranslucentBackground failures)
|
// - Arrow direction auto-detection (above/below based on screen space)
|
||||||
// - Leave-event resilience (catches spurious dismiss on tooltip popup)
|
// - Arrow X clamped to stay within rounded corners
|
||||||
// - Dismiss correctness (cursor truly leaves trigger zone)
|
// - WA_TranslucentBackground rendering (arrow + body have opaque pixels,
|
||||||
|
// corners are transparent)
|
||||||
|
// - Content sizing (title + separator + body)
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
class TestTooltip : public QObject {
|
class TestTooltip : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QWidget* m_window = nullptr;
|
QWidget* m_window = nullptr;
|
||||||
QPushButton* m_btnTop = nullptr;
|
RcxTooltip* m_tip = nullptr;
|
||||||
QPushButton* m_btnMid = nullptr;
|
|
||||||
QPushButton* m_btnLeft = nullptr;
|
|
||||||
QPushButton* m_btnRight= nullptr;
|
|
||||||
|
|
||||||
void showAndProcess(QWidget* trigger, const QString& text) {
|
QFont testFont() {
|
||||||
RcxTooltip::instance()->showFor(trigger, text);
|
QFont f("JetBrains Mono", 12);
|
||||||
// Process events + allow paint to complete
|
f.setFixedPitch(true);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAndProcess(const QPoint& anchor) {
|
||||||
|
m_tip->showAt(anchor);
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
QTest::qWait(20);
|
QTest::qWait(20);
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count non-transparent pixels in a QImage region
|
|
||||||
int countOpaquePixels(const QImage& img, const QRect& region) {
|
int countOpaquePixels(const QImage& img, const QRect& region) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
QRect r = region.intersected(img.rect());
|
QRect r = region.intersected(img.rect());
|
||||||
@@ -49,382 +52,180 @@ private slots:
|
|||||||
void initTestCase() {
|
void initTestCase() {
|
||||||
m_window = new QWidget;
|
m_window = new QWidget;
|
||||||
m_window->setFixedSize(800, 600);
|
m_window->setFixedSize(800, 600);
|
||||||
|
|
||||||
QScreen* scr = QApplication::primaryScreen();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
QRect avail = scr->availableGeometry();
|
QRect avail = scr->availableGeometry();
|
||||||
m_window->move(avail.center() - QPoint(400, 300));
|
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();
|
m_window->show();
|
||||||
QVERIFY(QTest::qWaitForWindowExposed(m_window));
|
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() {
|
void cleanupTestCase() {
|
||||||
RcxTooltip::instance()->dismiss();
|
m_tip->dismiss();
|
||||||
|
delete m_tip;
|
||||||
delete m_window;
|
delete m_window;
|
||||||
m_window = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanup() {
|
void cleanup() {
|
||||||
RcxTooltip::instance()->dismiss();
|
m_tip->dismiss();
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Singleton ──
|
|
||||||
void testSingleton() {
|
|
||||||
QCOMPARE(RcxTooltip::instance(), RcxTooltip::instance());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Basic show/dismiss ──
|
// ── Basic show/dismiss ──
|
||||||
void testShowAndDismiss() {
|
void testShowAndDismiss() {
|
||||||
auto* tip = RcxTooltip::instance();
|
QVERIFY(!m_tip->isVisible());
|
||||||
QVERIFY(!tip->isVisible());
|
m_tip->populate("Title", "Body text", testFont());
|
||||||
|
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
|
||||||
showAndProcess(m_btnMid, "Hello");
|
QVERIFY(m_tip->isVisible());
|
||||||
QVERIFY(tip->isVisible());
|
m_tip->dismiss();
|
||||||
QCOMPARE(tip->currentText(), QString("Hello"));
|
QVERIFY(!m_tip->isVisible());
|
||||||
QCOMPARE(tip->currentTrigger(), m_btnMid);
|
|
||||||
|
|
||||||
tip->dismiss();
|
|
||||||
QVERIFY(!tip->isVisible());
|
|
||||||
QVERIFY(tip->currentTrigger() == nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Empty text / null trigger = dismiss ──
|
// ── Duplicate populate is no-op ──
|
||||||
void testEmptyTextDismisses() {
|
void testDuplicatePopulateSkipped() {
|
||||||
auto* tip = RcxTooltip::instance();
|
m_tip->populate("Title", "Body", testFont());
|
||||||
showAndProcess(m_btnMid, "Test");
|
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
|
||||||
QVERIFY(tip->isVisible());
|
QPoint pos1 = m_tip->pos();
|
||||||
showAndProcess(m_btnMid, "");
|
// Same content — populate returns early, position unchanged
|
||||||
QVERIFY(!tip->isVisible());
|
m_tip->populate("Title", "Body", testFont());
|
||||||
|
QCOMPARE(m_tip->pos(), pos1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testNullTriggerDismisses() {
|
// ── Arrow direction: below when room exists ──
|
||||||
auto* tip = RcxTooltip::instance();
|
void testArrowUpWhenBelow() {
|
||||||
showAndProcess(m_btnMid, "Test");
|
m_tip->populate("Test", "Below", testFont());
|
||||||
QVERIFY(tip->isVisible());
|
// Anchor in middle of screen — plenty of room below
|
||||||
showAndProcess(nullptr, "Test");
|
QPoint anchor = m_window->mapToGlobal(QPoint(400, 300));
|
||||||
QVERIFY(!tip->isVisible());
|
showAndProcess(anchor);
|
||||||
|
QVERIFY(m_tip->isVisible());
|
||||||
|
// Arrow up (tooltip below anchor): widget top == anchor.y
|
||||||
|
QCOMPARE(m_tip->y(), anchor.y());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Arrow direction ──
|
// ── Arrow direction: above when no room below ──
|
||||||
void testArrowDownByDefault() {
|
void testArrowDownWhenAbove() {
|
||||||
auto* tip = RcxTooltip::instance();
|
m_tip->populate("Test", "Above", testFont());
|
||||||
showAndProcess(m_btnMid, "Default placement");
|
// Anchor near bottom of screen
|
||||||
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() {
|
|
||||||
QScreen* scr = QApplication::primaryScreen();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
QRect avail = scr->availableGeometry();
|
QRect avail = scr->availableGeometry();
|
||||||
QPoint oldPos = m_window->pos();
|
QPoint anchor(avail.center().x(), avail.bottom() - 5);
|
||||||
m_window->move(avail.center().x() - 400, avail.top());
|
showAndProcess(anchor);
|
||||||
QCoreApplication::processEvents();
|
QVERIFY(m_tip->isVisible());
|
||||||
|
// Arrow down (tooltip above anchor): widget bottom == anchor.y
|
||||||
auto* tip = RcxTooltip::instance();
|
int tipBottom = m_tip->y() + m_tip->height();
|
||||||
showAndProcess(m_btnTop, "Flipped");
|
QCOMPARE(tipBottom, anchor.y());
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Horizontal clamping ──
|
// ── Horizontal clamping ──
|
||||||
void testHorizontalClampLeft() {
|
void testHorizontalClampLeft() {
|
||||||
|
m_tip->populate("Test", "Wide body text for clamping", testFont());
|
||||||
QScreen* scr = QApplication::primaryScreen();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
QRect avail = scr->availableGeometry();
|
QRect avail = scr->availableGeometry();
|
||||||
QPoint oldPos = m_window->pos();
|
QPoint anchor(avail.left() + 5, avail.center().y());
|
||||||
m_window->move(avail.left(), avail.center().y() - 300);
|
showAndProcess(anchor);
|
||||||
QCoreApplication::processEvents();
|
QVERIFY(m_tip->x() >= avail.left());
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testHorizontalClampRight() {
|
void testHorizontalClampRight() {
|
||||||
|
m_tip->populate("Test", "Wide body text for clamping", testFont());
|
||||||
QScreen* scr = QApplication::primaryScreen();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
QRect avail = scr->availableGeometry();
|
QRect avail = scr->availableGeometry();
|
||||||
QPoint oldPos = m_window->pos();
|
QPoint anchor(avail.right() - 5, avail.center().y());
|
||||||
m_window->move(avail.right() - m_window->width(), avail.center().y() - 300);
|
showAndProcess(anchor);
|
||||||
QCoreApplication::processEvents();
|
QVERIFY(m_tip->x() + m_tip->width() <= avail.right() + 2);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ──
|
// ── Constants ──
|
||||||
void testConstants() {
|
void testConstants() {
|
||||||
QCOMPARE(RcxTooltip::kArrowH, 6);
|
QCOMPARE(RcxTooltip::kArrowH, 8);
|
||||||
QCOMPARE(RcxTooltip::kArrowHalfW, 6);
|
QCOMPARE(RcxTooltip::kArrowW, 14);
|
||||||
QCOMPARE(RcxTooltip::kGap, 2);
|
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() {
|
void testBodyRendersOpaquePixels() {
|
||||||
// Show tooltip and grab its rendered pixels.
|
m_tip->populate("Render", "Test body", testFont());
|
||||||
// Verify that the body area has non-transparent content.
|
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
|
||||||
auto* tip = RcxTooltip::instance();
|
QVERIFY(m_tip->isVisible());
|
||||||
showAndProcess(m_btnMid, "Render test");
|
|
||||||
QVERIFY(tip->isVisible());
|
|
||||||
|
|
||||||
// Force full opacity so grab gets real pixels
|
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||||
tip->setWindowOpacity(1.0);
|
QVERIFY(!img.isNull());
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
// Check center of body for opaque pixels (avoid edges/corners)
|
||||||
QVERIFY2(!img.isNull(), "grab() returned null image");
|
QRect center(img.width() / 4, img.height() / 4,
|
||||||
QVERIFY2(img.width() > 0 && img.height() > 0, "grab() returned empty image");
|
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
|
void testCornersAreTransparent() {
|
||||||
QRect body = tip->bodyRect();
|
m_tip->populate("Corner", "Test", testFont());
|
||||||
// Inset by 2px to avoid anti-aliased border edges
|
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
|
||||||
QRect checkRect = body.adjusted(2, 2, -2, -2);
|
QVERIFY(m_tip->isVisible());
|
||||||
int opaquePixels = countOpaquePixels(img, checkRect);
|
|
||||||
int totalPixels = checkRect.width() * checkRect.height();
|
|
||||||
|
|
||||||
QVERIFY2(opaquePixels > totalPixels / 2,
|
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||||
qPrintable(QStringLiteral(
|
|
||||||
"Body area has too few opaque pixels: %1 / %2 (< 50%%). "
|
// Top-left 2x2 corner should be fully transparent (rounded corner)
|
||||||
"The tooltip is not rendering its background.")
|
QRect corner(0, 0, 2, 2);
|
||||||
.arg(opaquePixels).arg(totalPixels)));
|
int opaque = countOpaquePixels(img, corner);
|
||||||
|
QCOMPARE(opaque, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testArrowRendersPixels() {
|
void testArrowRendersPixels() {
|
||||||
// Verify the triangle arrow region has some opaque pixels.
|
m_tip->populate("Arrow", "Test", testFont());
|
||||||
auto* tip = RcxTooltip::instance();
|
// Show below (arrow up) — arrow is in the top strip
|
||||||
showAndProcess(m_btnMid, "Arrow test");
|
showAndProcess(m_window->mapToGlobal(QPoint(400, 300)));
|
||||||
QVERIFY(tip->isVisible());
|
QVERIFY(m_tip->isVisible());
|
||||||
QVERIFY(tip->arrowPointsDown());
|
|
||||||
|
|
||||||
tip->setWindowOpacity(1.0);
|
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
QImage img = tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
// Arrow region: top kArrowH pixels, centered horizontally
|
||||||
|
int centerX = img.width() / 2;
|
||||||
// Arrow region: below the body rect, centered on arrowLocalX
|
QRect arrowRect(centerX - RcxTooltip::kArrowW / 2, 0,
|
||||||
QRect body = tip->bodyRect();
|
RcxTooltip::kArrowW, RcxTooltip::kArrowH);
|
||||||
int arrowTop = body.bottom();
|
int opaque = countOpaquePixels(img, arrowRect);
|
||||||
int arrowLeft = tip->arrowLocalX() - RcxTooltip::kArrowHalfW;
|
QVERIFY2(opaque > 0,
|
||||||
int arrowRight = tip->arrowLocalX() + RcxTooltip::kArrowHalfW;
|
qPrintable(QStringLiteral("Arrow region has 0 opaque pixels")));
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,290 +1,106 @@
|
|||||||
// Tests the full tooltip flow including DarkApp-style ToolTip interception.
|
// Tests RcxTooltip positioning and arrow direction across screen edges.
|
||||||
// Verifies that QEvent::ToolTip fires and our custom tooltip appears.
|
// Validates that the arrow tip touches the anchor point and the tooltip
|
||||||
|
// body stays within screen bounds.
|
||||||
|
|
||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QHelpEvent>
|
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include "rcxtooltip.h"
|
#include "rcxtooltip.h"
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
using namespace rcx;
|
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 {
|
class TestTooltipEvent : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QWidget* m_window = nullptr;
|
RcxTooltip* m_tip = nullptr;
|
||||||
QPushButton* m_btn = nullptr;
|
|
||||||
QPushButton* m_btn2 = nullptr;
|
QFont testFont() {
|
||||||
DarkAppSimulator* m_sim = nullptr;
|
QFont f("JetBrains Mono", 12);
|
||||||
|
f.setFixedPitch(true);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void initTestCase() {
|
void initTestCase() {
|
||||||
LOG("=== TestTooltipEvent starting ===\n");
|
m_tip = new RcxTooltip;
|
||||||
|
const auto& t = ThemeManager::instance().current();
|
||||||
m_window = new QWidget;
|
m_tip->setTheme(t.backgroundAlt, t.border, t.text, t.syntaxNumber, t.border);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanupTestCase() {
|
void cleanupTestCase() {
|
||||||
qApp->removeEventFilter(m_sim);
|
m_tip->dismiss();
|
||||||
RcxTooltip::instance()->dismiss();
|
delete m_tip;
|
||||||
delete m_sim;
|
|
||||||
delete m_window;
|
|
||||||
LOG("=== TestTooltipEvent finished ===\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanup() {
|
void cleanup() {
|
||||||
RcxTooltip::instance()->dismiss();
|
m_tip->dismiss();
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
m_sim->tooltipEventCount = 0;
|
|
||||||
m_sim->leaveEventCount = 0;
|
|
||||||
m_sim->showForCallCount = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 1: Post QHelpEvent → DarkApp simulator intercepts → RcxTooltip shows
|
// Arrow tip Y matches anchor Y when showing below
|
||||||
void testManualEventShowsTooltip() {
|
void testArrowTipMatchesAnchorBelow() {
|
||||||
LOG("\n--- testManualEventShowsTooltip ---\n");
|
m_tip->populate("Test", "Body", testFont());
|
||||||
auto* tip = RcxTooltip::instance();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
|
QPoint anchor = scr->availableGeometry().center();
|
||||||
QPoint btnGlobal = m_btn->mapToGlobal(QPoint(60, 20));
|
m_tip->showAt(anchor);
|
||||||
QCursor::setPos(btnGlobal);
|
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
|
QVERIFY(m_tip->isVisible());
|
||||||
LOG(" posting QHelpEvent\n");
|
// Arrow up (tooltip below): widget top == anchor.y
|
||||||
QHelpEvent helpEvent(QEvent::ToolTip, QPoint(60, 20), btnGlobal);
|
QCOMPARE(m_tip->y(), anchor.y());
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 2: Qt's native tooltip timer fires → our filter intercepts → tooltip shows
|
// Arrow tip Y matches anchor Y when showing above
|
||||||
void testNativeTimerShowsTooltip() {
|
void testArrowTipMatchesAnchorAbove() {
|
||||||
LOG("\n--- testNativeTimerShowsTooltip ---\n");
|
m_tip->populate("Test", "Body", testFont());
|
||||||
auto* tip = RcxTooltip::instance();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
|
QRect avail = scr->availableGeometry();
|
||||||
// Move cursor away first
|
QPoint anchor(avail.center().x(), avail.bottom() - 2);
|
||||||
QPoint away = m_window->mapToGlobal(QPoint(380, 10));
|
m_tip->showAt(anchor);
|
||||||
QCursor::setPos(away);
|
|
||||||
QTest::qWait(200);
|
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
|
QVERIFY(m_tip->isVisible());
|
||||||
// Move to button
|
// Arrow down (tooltip above): widget bottom == anchor.y
|
||||||
QPoint btnCenter = m_btn->mapToGlobal(QPoint(60, 20));
|
QCOMPARE(m_tip->y() + m_tip->height(), anchor.y());
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 3: Leave after tooltip shown → tooltip survives (cursor still in zone)
|
// Tooltip stays within screen bounds at left edge
|
||||||
void testLeaveSurvival() {
|
void testScreenLeftEdge() {
|
||||||
LOG("\n--- testLeaveSurvival ---\n");
|
m_tip->populate("Test", "Wide body content for edge test", testFont());
|
||||||
auto* tip = RcxTooltip::instance();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
|
QRect avail = scr->availableGeometry();
|
||||||
QPoint btnCenter = m_btn->mapToGlobal(QPoint(60, 20));
|
QPoint anchor(avail.left() + 2, avail.center().y());
|
||||||
QCursor::setPos(btnCenter);
|
m_tip->showAt(anchor);
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
|
QVERIFY(m_tip->x() >= avail.left());
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 4: Switch between widgets
|
// Tooltip stays within screen bounds at right edge
|
||||||
void testWidgetSwitch() {
|
void testScreenRightEdge() {
|
||||||
LOG("\n--- testWidgetSwitch ---\n");
|
m_tip->populate("Test", "Wide body content for edge test", testFont());
|
||||||
auto* tip = RcxTooltip::instance();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
|
QRect avail = scr->availableGeometry();
|
||||||
// Show on btn1
|
QPoint anchor(avail.right() - 2, avail.center().y());
|
||||||
QPoint btn1Center = m_btn->mapToGlobal(QPoint(60, 20));
|
m_tip->showAt(anchor);
|
||||||
QCursor::setPos(btn1Center);
|
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
QHelpEvent ev1(QEvent::ToolTip, QPoint(60, 20), btn1Center);
|
QVERIFY(m_tip->x() + m_tip->width() <= avail.right() + 2);
|
||||||
QApplication::sendEvent(m_btn, &ev1);
|
}
|
||||||
QCoreApplication::processEvents();
|
|
||||||
QTest::qWait(100);
|
|
||||||
QVERIFY(tip->isVisible());
|
|
||||||
QCOMPARE(tip->currentText(), QString("Start scanning memory"));
|
|
||||||
QPoint pos1 = tip->pos();
|
|
||||||
|
|
||||||
// Switch to btn2
|
// Content change triggers resize
|
||||||
QPoint btn2Center = m_btn2->mapToGlobal(QPoint(60, 20));
|
void testContentResize() {
|
||||||
QCursor::setPos(btn2Center);
|
m_tip->populate("Short", "A", testFont());
|
||||||
|
m_tip->showAt(QPoint(500, 500));
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
QHelpEvent ev2(QEvent::ToolTip, QPoint(60, 20), btn2Center);
|
int w1 = m_tip->width();
|
||||||
QApplication::sendEvent(m_btn2, &ev2);
|
|
||||||
|
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();
|
QCoreApplication::processEvents();
|
||||||
QTest::qWait(100);
|
int w2 = m_tip->width();
|
||||||
|
|
||||||
LOG(" after switch: visible=%d text='%s' pos=(%d,%d)\n",
|
QVERIFY2(w2 > w1, "Wider content should produce a wider tooltip");
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,251 +1,126 @@
|
|||||||
// Integration test: simulates the full tooltip flow as DarkApp would see it.
|
// Rendering verification for RcxTooltip.
|
||||||
// Posts QHelpEvent (ToolTip), sends Leave events, verifies RcxTooltip behavior
|
// Grabs widget pixels to confirm WA_TranslucentBackground works correctly
|
||||||
// with fprintf at every stage so we can see exactly what happens.
|
// and the arrow/body are painted with the expected alpha.
|
||||||
|
|
||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QPushButton>
|
|
||||||
#include <QHelpEvent>
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include "rcxtooltip.h"
|
#include "rcxtooltip.h"
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
using namespace rcx;
|
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 {
|
class TestTooltipUI : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QWidget* m_window = nullptr;
|
RcxTooltip* m_tip = nullptr;
|
||||||
QPushButton* m_btn = nullptr;
|
|
||||||
QPushButton* m_btn2 = 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:
|
private slots:
|
||||||
void initTestCase() {
|
void initTestCase() {
|
||||||
LOG("=== TestTooltipUI starting ===\n");
|
m_tip = new RcxTooltip;
|
||||||
|
const auto& t = ThemeManager::instance().current();
|
||||||
m_window = new QWidget;
|
m_tip->setTheme(t.backgroundAlt, t.border, t.text, t.syntaxNumber, t.border);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanupTestCase() {
|
void cleanupTestCase() {
|
||||||
RcxTooltip::instance()->dismiss();
|
m_tip->dismiss();
|
||||||
delete m_window;
|
delete m_tip;
|
||||||
LOG("=== TestTooltipUI finished ===\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanup() {
|
void cleanup() {
|
||||||
RcxTooltip::instance()->dismiss();
|
m_tip->dismiss();
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Test 1: Full tooltip lifecycle with event simulation ───
|
// Body center should be opaque (background painted)
|
||||||
void testFullLifecycle() {
|
void testBodyIsOpaque() {
|
||||||
LOG("\n--- testFullLifecycle ---\n");
|
m_tip->populate("Render Test", "Body content here", testFont());
|
||||||
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");
|
|
||||||
QScreen* scr = QApplication::primaryScreen();
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
QPoint farAway = scr->availableGeometry().bottomRight() - QPoint(50, 50);
|
m_tip->showAt(scr->availableGeometry().center());
|
||||||
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);
|
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
QTest::qWait(50);
|
QTest::qWait(50);
|
||||||
|
|
||||||
LOG(" switch to btn2 immediately\n");
|
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||||
QCursor::setPos(m_btn2->mapToGlobal(QPoint(40, 14)));
|
QVERIFY(!img.isNull());
|
||||||
QCoreApplication::processEvents();
|
|
||||||
simulateDarkAppToolTip(m_btn2);
|
|
||||||
QCoreApplication::processEvents();
|
|
||||||
QTest::qWait(100);
|
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
LOG(" visible=%d text='%s'\n", tip->isVisible(), qPrintable(tip->currentText()));
|
// Center 50% of widget should be mostly opaque
|
||||||
QVERIFY(tip->isVisible());
|
QRect center(img.width() / 4, img.height() / 4,
|
||||||
QCOMPARE(tip->currentText(), QString("Copy address to clipboard"));
|
img.width() / 2, img.height() / 2);
|
||||||
LOG("--- testRapidSwitch PASSED ---\n");
|
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 ───
|
// Top-left corner should be transparent (rounded corner + WA_TranslucentBackground)
|
||||||
void testNoTooltipWidget() {
|
void testCornerTransparency() {
|
||||||
LOG("\n--- testNoTooltipWidget ---\n");
|
m_tip->populate("Corner", "Test", testFont());
|
||||||
QPushButton noTip("NoTip", m_window);
|
QScreen* scr = QApplication::primaryScreen();
|
||||||
noTip.setFixedSize(80, 28);
|
m_tip->showAt(scr->availableGeometry().center());
|
||||||
noTip.move(50, 50);
|
QCoreApplication::processEvents();
|
||||||
noTip.show();
|
QTest::qWait(50);
|
||||||
// No setToolTip called
|
|
||||||
|
|
||||||
auto* tip = RcxTooltip::instance();
|
QImage img = m_tip->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||||
bool handled = simulateDarkAppToolTip(&noTip);
|
|
||||||
LOG(" handled=%d visible=%d\n", handled, tip->isVisible());
|
// When arrow is up, body starts at kArrowH. The corner at (0, kArrowH)
|
||||||
QVERIFY(!handled);
|
// should be transparent due to rounding.
|
||||||
QVERIFY(!tip->isVisible());
|
QRect corner(0, 0, 2, 2);
|
||||||
LOG("--- testNoTooltipWidget PASSED ---\n");
|
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