Compare commits
111 Commits
snapshot-0
...
mac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4727df3e9 | ||
|
|
dc6963e0d5 | ||
|
|
cb10bc8a82 | ||
|
|
b5521bd638 | ||
|
|
89d6e1944b | ||
|
|
7528d1bbbb | ||
|
|
4f2288048e | ||
|
|
97b6f55e1f | ||
|
|
6a30e0a402 | ||
|
|
1501a1542c | ||
|
|
4f82b39785 | ||
|
|
009ddc951c | ||
|
|
5921af2b4f | ||
|
|
5ded192990 | ||
|
|
54bee5022b | ||
|
|
5d2d324946 | ||
|
|
5b2cf1ae1f | ||
|
|
f1a36f2ad3 | ||
|
|
665138e688 | ||
|
|
7688bb5b92 | ||
|
|
701e088be8 | ||
|
|
3c0c248d54 | ||
|
|
7af969f6bd | ||
|
|
8ba1fd2492 | ||
|
|
b08736245b | ||
|
|
7f7bbdcc45 | ||
|
|
79b5125229 | ||
|
|
3aeb1a80d5 | ||
|
|
3b7ed682ac | ||
|
|
0582cb286b | ||
|
|
ea85b7a621 | ||
|
|
6c8b7d3d97 | ||
|
|
d1321b5165 | ||
|
|
483f87cfbd | ||
|
|
4d0782db68 | ||
|
|
51de48a6ed | ||
|
|
7b9b140823 | ||
|
|
a21e5a07a8 | ||
|
|
25afbe373b | ||
|
|
6a4cb47ed4 | ||
|
|
431e2b90c9 | ||
|
|
43365c1aff | ||
|
|
596f410b96 | ||
|
|
f0fc85f60f | ||
|
|
70c7404556 | ||
|
|
f27459c21b | ||
|
|
a5abcbeea6 | ||
|
|
7071402319 | ||
|
|
0dc390ed86 | ||
|
|
188c27c6e2 | ||
|
|
81f1e4319f | ||
|
|
3ab6affa5e | ||
|
|
35b3cd9ac1 | ||
|
|
e5938f7e82 | ||
|
|
03c49d19dd | ||
|
|
b7eebedf50 | ||
|
|
9ff456a8d6 | ||
|
|
580f285edd | ||
|
|
d23a6c7656 | ||
|
|
25d8de95b7 | ||
|
|
955db3813a | ||
|
|
f4f203e0f0 | ||
|
|
1d3f1a672a | ||
|
|
da29206bdb | ||
|
|
4986893fca | ||
|
|
17a1fb032e | ||
|
|
8d92957837 | ||
|
|
f981fe456d | ||
|
|
877ceea4c1 | ||
|
|
4160a229c6 | ||
|
|
1e1afc1640 | ||
|
|
f0cf6c549a | ||
|
|
683eab16ee | ||
|
|
b53dea8f9f | ||
|
|
f06abbab79 | ||
|
|
2477591ed2 | ||
|
|
6c13356d6d | ||
|
|
3b273a7ab2 | ||
|
|
3509a0d9dd | ||
|
|
43c3f5a842 | ||
|
|
0697ce4853 | ||
|
|
ed1bfd04cd | ||
|
|
c275eb33c9 | ||
|
|
636176ee8c | ||
|
|
9a716444f4 | ||
|
|
a46da4ee16 | ||
|
|
cd52451210 | ||
|
|
82bf9118c9 | ||
|
|
f4c7e9327d | ||
|
|
5944dbdc81 | ||
|
|
b3425aec9e | ||
|
|
2a8cfee719 | ||
|
|
e999c664b8 | ||
|
|
0dc4af6b1d | ||
|
|
376aad2169 | ||
|
|
4937c58062 | ||
|
|
9c72265901 | ||
|
|
86499e58ee | ||
|
|
b2ae8d5a5d | ||
|
|
6768f04e9a | ||
|
|
c6e5f6508f | ||
|
|
e6529052b3 | ||
|
|
d43e989992 | ||
|
|
879e9f4047 | ||
|
|
e0d5a799b4 | ||
|
|
efae193520 | ||
|
|
ba1c2f8e5a | ||
|
|
5a0a4d1802 | ||
|
|
030eb34510 | ||
|
|
2939b25895 | ||
|
|
d38cb02fa2 |
103
.github/workflows/build.yml
vendored
@@ -2,7 +2,8 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
@@ -21,9 +22,9 @@ jobs:
|
||||
- name: Install Qt6 and MinGW
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
arch: 'win64_mingw'
|
||||
tools: 'tools_mingw1310,qt.tools.win64_mingw1310'
|
||||
version: "6.8.1"
|
||||
arch: "win64_mingw"
|
||||
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
|
||||
cache: true
|
||||
|
||||
- name: Configure
|
||||
@@ -40,6 +41,36 @@ jobs:
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
cmake --build build
|
||||
|
||||
- name: Install WDK NuGet
|
||||
shell: pwsh
|
||||
run: |
|
||||
nuget install Microsoft.Windows.WDK.x64 -OutputDirectory wdk_pkg
|
||||
$ntddk = Get-ChildItem wdk_pkg -Recurse -Filter "ntddk.h" |
|
||||
Where-Object { $_.DirectoryName -like "*km*" } |
|
||||
Select-Object -First 1
|
||||
if (!$ntddk) { throw "ntddk.h not found in WDK NuGet package" }
|
||||
$kmDir = $ntddk.DirectoryName
|
||||
$incRoot = Split-Path $kmDir -Parent
|
||||
Write-Host "WDK include root: $incRoot"
|
||||
echo "WDK_INC_ROOT=$incRoot" >> $env:GITHUB_ENV
|
||||
$ntos = Get-ChildItem wdk_pkg -Recurse -Filter "ntoskrnl.lib" |
|
||||
Where-Object { $_.DirectoryName -like "*x64*" } |
|
||||
Select-Object -First 1
|
||||
if (!$ntos) { throw "ntoskrnl.lib not found in WDK NuGet package" }
|
||||
$libRoot = Split-Path (Split-Path $ntos.DirectoryName -Parent) -Parent
|
||||
Write-Host "WDK lib root: $libRoot"
|
||||
echo "WDK_LIB_ROOT=$libRoot" >> $env:GITHUB_ENV
|
||||
$specstr = Get-ChildItem wdk_pkg -Recurse -Filter "specstrings.h" |
|
||||
Select-Object -First 1
|
||||
if (!$specstr) { throw "specstrings.h not found in SDK NuGet package" }
|
||||
$sdkIncRoot = Split-Path $specstr.DirectoryName -Parent
|
||||
Write-Host "SDK include root: $sdkIncRoot"
|
||||
echo "SDK_INC_ROOT=$sdkIncRoot" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Build kernel driver
|
||||
shell: cmd
|
||||
run: call plugins\KernelMemory\driver\build_driver.bat
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -61,6 +92,7 @@ jobs:
|
||||
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
|
||||
mkdir -p release/Plugins
|
||||
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
|
||||
cp plugins/KernelMemory/driver/build/rcxdrv.sys release/Plugins/ 2>/dev/null || true
|
||||
cp -r build/themes release/ 2>/dev/null || true
|
||||
cp -r build/examples release/ 2>/dev/null || true
|
||||
cp build/screenshot.png release/ 2>/dev/null || true
|
||||
@@ -83,7 +115,7 @@ jobs:
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
version: "6.8.1"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -140,9 +172,66 @@ jobs:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
macos:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-15
|
||||
qt_arch: clang_arm64
|
||||
artifact_name: Reclass-macos-arm64-qt6
|
||||
zip_name: Reclass-macos-arm64-qt6.zip
|
||||
- os: macos-15-intel
|
||||
qt_arch: clang_64
|
||||
artifact_name: Reclass-macos-x86_64-qt6
|
||||
zip_name: Reclass-macos-x86_64-qt6.zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install cmake ninja qt
|
||||
|
||||
- name: Configure Qt paths
|
||||
run: |
|
||||
QT_PREFIX="$(brew --prefix qt)"
|
||||
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
|
||||
echo "PATH=$QT_PREFIX/bin:$PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Package app zip
|
||||
run: |
|
||||
MACDEPLOYQT_BIN="$QT_PREFIX/bin/macdeployqt"
|
||||
if [ ! -x "$MACDEPLOYQT_BIN" ]; then
|
||||
MACDEPLOYQT_BIN=$(which macdeployqt 2>/dev/null || find "$RUNNER_WORKSPACE" -name macdeployqt -path "*/bin/*" | head -1)
|
||||
fi
|
||||
echo "Found macdeployqt at: $MACDEPLOYQT_BIN"
|
||||
"$MACDEPLOYQT_BIN" build/Reclass.app -always-overwrite
|
||||
codesign --force --deep --sign - build/Reclass.app
|
||||
ditto -c -k --sequesterRsrc --keepParent build/Reclass.app "${{ matrix.zip_name }}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.zip_name }}
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [windows, linux]
|
||||
needs: [windows, linux, macos]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -167,5 +256,7 @@ jobs:
|
||||
files: |
|
||||
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
||||
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
|
||||
artifacts/Reclass-macos-arm64-qt6/Reclass-macos-arm64-qt6.zip
|
||||
artifacts/Reclass-macos-x86_64-qt6/Reclass-macos-x86_64-qt6.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
@@ -14,3 +14,4 @@ CMakeUserPresets.json
|
||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||
.cache
|
||||
*.DS_Store
|
||||
|
||||
380
CMakeLists.txt
@@ -22,6 +22,32 @@ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${_QT_COMPONENTS})
|
||||
set(QT Qt${QT_VERSION_MAJOR})
|
||||
message(STATUS "Using ${QT}: ${${QT}_DIR}")
|
||||
|
||||
# ── ABI sanity check: prevent MSVC ↔ MinGW Qt mismatch ──
|
||||
# Building with MSVC against MinGW Qt (or vice versa) compiles fine but
|
||||
# crashes immediately at runtime (ABI mismatch in QString/QSettings internals).
|
||||
if(MSVC AND "${${QT}_DIR}" MATCHES "mingw")
|
||||
message(FATAL_ERROR
|
||||
"Qt installation was built with MinGW but this project is being compiled with MSVC.\n"
|
||||
" Qt found at: ${${QT}_DIR}\n"
|
||||
"This will compile but crash at startup due to ABI mismatch.\n"
|
||||
"Fix: install Qt for MSVC (e.g. msvc2019_64) and set CMAKE_PREFIX_PATH to it:\n"
|
||||
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/msvc2019_64 ..")
|
||||
elseif(MINGW AND "${${QT}_DIR}" MATCHES "msvc")
|
||||
message(FATAL_ERROR
|
||||
"Qt installation was built with MSVC but this project is being compiled with MinGW.\n"
|
||||
" Qt found at: ${${QT}_DIR}\n"
|
||||
"This will compile but crash at startup due to ABI mismatch.\n"
|
||||
"Fix: install Qt for MinGW and set CMAKE_PREFIX_PATH to it:\n"
|
||||
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/mingw_64 ..")
|
||||
endif()
|
||||
|
||||
# ── MSVC compile flags ──
|
||||
if(MSVC)
|
||||
# /utf-8: treat source and execution character sets as UTF-8
|
||||
# /MP: multi-processor compilation
|
||||
add_compile_options(/utf-8 /MP)
|
||||
endif()
|
||||
|
||||
# Qt5 on Windows needs WinExtras for HICON conversion
|
||||
set(_QT_WINEXTRAS "")
|
||||
if(QT_VERSION_MAJOR EQUAL 5 AND WIN32)
|
||||
@@ -36,10 +62,35 @@ file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
|
||||
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
|
||||
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
|
||||
target_compile_features(raw_pdb PRIVATE cxx_std_11)
|
||||
# PDB_CRT.h forward-declares printf/memcmp/etc with __cdecl which conflicts
|
||||
# with non-MSVC compilers (GCC, Clang, MinGW). Force-include a prefix header
|
||||
# that pulls in the real CRT headers and strips __cdecl.
|
||||
if(NOT MSVC)
|
||||
target_compile_options(raw_pdb PUBLIC
|
||||
-include "${CMAKE_CURRENT_SOURCE_DIR}/cmake/raw_pdb_prefix.h")
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(raw_pdb PRIVATE rpcrt4)
|
||||
endif()
|
||||
|
||||
# Fadec — generate decode tables (.inc files) from instrs.txt at configure time
|
||||
find_package(Python3 3.9 REQUIRED)
|
||||
set(FADEC_DIR "${CMAKE_SOURCE_DIR}/third_party/fadec")
|
||||
if(NOT EXISTS "${FADEC_DIR}/fadec-decode-public.inc")
|
||||
message(STATUS "Generating fadec decode tables...")
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} "${FADEC_DIR}/parseinstrs.py" decode
|
||||
"${FADEC_DIR}/instrs.txt"
|
||||
"${FADEC_DIR}/fadec-decode-public.inc"
|
||||
"${FADEC_DIR}/fadec-decode-private.inc"
|
||||
--32 --64
|
||||
RESULT_VARIABLE _fadec_result
|
||||
)
|
||||
if(NOT _fadec_result EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to generate fadec decode tables")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(Reclass
|
||||
src/main.cpp
|
||||
src/editor.h
|
||||
@@ -84,14 +135,24 @@ add_executable(Reclass
|
||||
src/scannerpanel.h
|
||||
src/scannerpanel.cpp
|
||||
src/mainwindow.h
|
||||
src/startpage.h
|
||||
src/dock_tab_buttons.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/macos_titlebar.h
|
||||
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/addressparser.h
|
||||
src/addressparser.cpp
|
||||
src/symbolstore.h
|
||||
src/symbolstore.cpp
|
||||
src/symbol_downloader.h
|
||||
src/symbol_downloader.cpp
|
||||
src/imports/pe_debug_info.h
|
||||
src/imports/pe_debug_info.cpp
|
||||
src/disasm.h
|
||||
src/disasm.cpp
|
||||
third_party/fadec/decode.c
|
||||
@@ -99,6 +160,16 @@ add_executable(Reclass
|
||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(Reclass PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_ICON_FILE "class.icns"
|
||||
)
|
||||
target_sources(Reclass PRIVATE src/icons/class.icns)
|
||||
set_source_files_properties(src/icons/class.icns
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||
endif()
|
||||
|
||||
target_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||
|
||||
target_link_libraries(Reclass PRIVATE
|
||||
@@ -112,27 +183,98 @@ target_link_libraries(Reclass PRIVATE
|
||||
)
|
||||
if(WIN32)
|
||||
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
|
||||
|
||||
# Copy Debugging Tools dbghelp.dll next to Reclass.exe so the Windows
|
||||
# loader picks it up (app dir > System32). The system dbghelp.dll
|
||||
# lacks StackWalk2 which the tools dbgeng.dll needs for remote debug.
|
||||
set(_DBG_TOOLS_DIRS
|
||||
"C:/Program Files (x86)/Windows Kits/10/Debuggers/x64"
|
||||
"C:/Program Files/Windows Kits/10/Debuggers/x64")
|
||||
foreach(_dir ${_DBG_TOOLS_DIRS})
|
||||
if(EXISTS "${_dir}/dbghelp.dll")
|
||||
foreach(_dll dbghelp.dll dbgcore.dll symsrv.dll)
|
||||
if(EXISTS "${_dir}/${_dll}")
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_dir}/${_dll}" $<TARGET_FILE_DIR:Reclass>
|
||||
COMMENT "Copying ${_dll} from Debugging Tools")
|
||||
endif()
|
||||
endforeach()
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
||||
target_link_libraries(ReclassMcpBridge PRIVATE ${QT}::Core ${QT}::Network)
|
||||
if(APPLE)
|
||||
add_custom_command(TARGET ReclassMcpBridge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
$<TARGET_FILE:ReclassMcpBridge>
|
||||
$<TARGET_FILE_DIR:Reclass>/ReclassMcpBridge
|
||||
COMMENT "Bundling ReclassMcpBridge into Reclass.app"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Copy built-in theme JSON files to build directory
|
||||
# Copy built-in theme JSON files next to the executable.
|
||||
# For single-config generators (Ninja/Make) the exe is in ${CMAKE_BINARY_DIR},
|
||||
# for multi-config generators (MSVC/Xcode) it's in ${CMAKE_BINARY_DIR}/<config>.
|
||||
# Using a post-build copy with $<TARGET_FILE_DIR:Reclass> handles both.
|
||||
file(GLOB _theme_files "${CMAKE_SOURCE_DIR}/src/themes/defaults/*.json")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
|
||||
|
||||
# Single-config: configure_file for IDE convenience (available before first build)
|
||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
|
||||
foreach(_tf ${_theme_files})
|
||||
get_filename_component(_name ${_tf} NAME)
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Post-build: always copy to the actual exe directory (works for all generators)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/themes"
|
||||
COMMENT "Creating themes directory next to executable")
|
||||
foreach(_tf ${_theme_files})
|
||||
get_filename_component(_name ${_tf} NAME)
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_tf}" "$<TARGET_FILE_DIR:Reclass>/themes/${_name}")
|
||||
endforeach()
|
||||
|
||||
# Copy example .rcx files to build directory
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_theme_files})
|
||||
set_source_files_properties(${_theme_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
|
||||
endif()
|
||||
|
||||
# Copy example .rcx files next to the executable (same logic as themes)
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
|
||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
foreach(_ef ${_example_files})
|
||||
get_filename_component(_name ${_ef} NAME)
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/examples"
|
||||
COMMENT "Creating examples directory next to executable")
|
||||
foreach(_ef ${_example_files})
|
||||
get_filename_component(_name ${_ef} NAME)
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_ef}" "$<TARGET_FILE_DIR:Reclass>/examples/${_name}")
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_example_files})
|
||||
set_source_files_properties(${_example_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
|
||||
endif()
|
||||
|
||||
include(deploy)
|
||||
|
||||
|
||||
@@ -178,6 +320,11 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_core COMMAND test_core)
|
||||
|
||||
add_executable(test_typeinfer tests/test_typeinfer.cpp)
|
||||
target_include_directories(test_typeinfer PRIVATE src)
|
||||
target_link_libraries(test_typeinfer PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_typeinfer COMMAND test_typeinfer)
|
||||
|
||||
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_format PRIVATE src)
|
||||
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
|
||||
@@ -273,178 +420,174 @@ if(BUILD_TESTING)
|
||||
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
|
||||
if(BUILD_UI_TESTS)
|
||||
|
||||
add_executable(test_controller tests/test_controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
add_executable(test_controller tests/test_controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.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/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_validation PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
|
||||
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
|
||||
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/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
target_include_directories(test_source_management PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_management PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_management COMMAND test_source_management)
|
||||
|
||||
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_source_management PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_management PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_management COMMAND test_source_management)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_new_features PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_new_features COMMAND test_new_features)
|
||||
|
||||
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
|
||||
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/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
|
||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
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/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_visibility PRIVATE
|
||||
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_visibility PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_visibility COMMAND test_type_visibility)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_visibility COMMAND test_type_visibility)
|
||||
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
|
||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
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/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
||||
src/resources.qrc)
|
||||
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_provider PRIVATE
|
||||
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
|
||||
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
|
||||
src/scanner.cpp src/scannerpanel.cpp src/addressparser.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_scanner_ui PRIVATE src)
|
||||
target_link_libraries(test_scanner_ui PRIVATE
|
||||
target_include_directories(test_scanner_ui PRIVATE src)
|
||||
target_link_libraries(test_scanner_ui PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
add_executable(test_mcp tests/test_mcp.cpp)
|
||||
target_include_directories(test_mcp PRIVATE src)
|
||||
target_link_libraries(test_mcp PRIVATE ${QT}::Core ${QT}::Network ${QT}::Test)
|
||||
add_test(NAME test_mcp COMMAND test_mcp)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
|
||||
src/scanner.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
endif()
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
|
||||
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||
add_executable(test_kernel_provider tests/test_kernel_provider.cpp
|
||||
plugins/KernelMemory/KernelMemoryPlugin.cpp
|
||||
src/processpicker.cpp src/processpicker.ui
|
||||
src/scanner.cpp)
|
||||
target_include_directories(test_kernel_provider PRIVATE
|
||||
src plugins/KernelMemory)
|
||||
target_link_libraries(test_kernel_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test
|
||||
psapi shell32 advapi32 ${_QT_WINEXTRAS})
|
||||
add_test(NAME test_kernel_provider COMMAND test_kernel_provider)
|
||||
endif()
|
||||
|
||||
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
|
||||
target_link_libraries(bench_large_class PRIVATE
|
||||
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
|
||||
target_link_libraries(bench_large_class PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||
|
||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
add_custom_target(deploy_tests ALL
|
||||
add_executable(bench_project tests/bench_project.cpp)
|
||||
target_include_directories(bench_project PRIVATE src)
|
||||
target_link_libraries(bench_project PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_project PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_project COMMAND bench_project)
|
||||
|
||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
add_custom_target(deploy_tests ALL
|
||||
COMMAND $<TARGET_FILE:${QT}::windeployqt>
|
||||
--no-compiler-runtime --no-translations
|
||||
--no-opengl-sw --no-system-d3d-compiler
|
||||
@@ -452,13 +595,16 @@ if(BUILD_TESTING)
|
||||
DEPENDS test_controller
|
||||
COMMENT "Deploying Qt runtime DLLs for tests..."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
if(NOT APPLE)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/KernelMemory)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
endif()
|
||||
|
||||
137
README.md
@@ -12,64 +12,117 @@
|
||||
[](https://github.com/IChooseYou/Reclass/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/IChooseYou/Reclass/releases)
|
||||
[]()
|
||||
[]()
|
||||
|
||||
</div>
|
||||
|
||||
Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is essentially a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump.
|
||||
Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump.
|
||||
|
||||
Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor surface is rendered as formatted plain text with inline editing, fold markers, and hex/ASCII previews.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Structured binary view** — render raw bytes as typed fields (integers, floats, pointers, vectors, matrices, strings, booleans, padding)
|
||||
- **Struct & array nesting** — define nested structs and arrays with collapsible fold regions
|
||||
- **Enums & bitfields** — define enums and bitfield types with named members, inline editing, and auto-sort
|
||||
- **Inline editing** — click to edit type names, field names, values, and base addresses directly in the editor
|
||||
- **Undo/redo** — full undo history for all mutations via command stack
|
||||
- **Multi-document tabs** — open multiple projects simultaneously in MDI sub-windows
|
||||
### Editor
|
||||
|
||||
- **Structured binary view** — render raw bytes as typed fields with columnar alignment
|
||||
- **Inline editing** — click to edit type names, field names, values, base addresses, array metadata, pointer targets, enum members, bitfield members, static expressions, and comments — all with real-time validation
|
||||
- **Tab-cycling** — tab through editable fields within a line
|
||||
- **Type autocomplete** — cached popup type picker with search/filter for struct targets
|
||||
- **Multi-select** — Ctrl+click individual nodes or Shift+click for range selection
|
||||
- **Split views** — multiple synchronized editor panes over the same document
|
||||
- **Type autocomplete** — popup type picker when changing field kinds
|
||||
- **Hex + ASCII margins** — raw byte previews alongside the structured view
|
||||
- **Value history & heatmap** — track value changes over time with color-coded heat indicators
|
||||
- **Disassembly preview** — hover over code pointers to see decoded instructions
|
||||
- **C/C++ code generation** — export structs as compilable C/C++ headers
|
||||
- **Import / export** — PDB import (Windows), ReClass XML import/export, C/C++ source import
|
||||
- **Themes** — built-in theme editor with multiple presets
|
||||
- **MCP bridge** — expose all tool functionality to AI clients via Model Context Protocol
|
||||
- **Plugin system** — extend with custom data source providers via DLL plugins; the following ship by default:
|
||||
- **Process plugin** — access memory of live processes on Windows and Linux
|
||||
- **WinDbg plugin** — access data sources live in WinDbg debugging sessions
|
||||
- **ReClass.NET compatibility layer** — load existing .NET and native ReClass.NET plugins
|
||||
- **Find bar** — Ctrl+F in-editor search with indicator highlighting
|
||||
- **Fold/collapse** — expand and collapse structs, arrays, and pointer expansions with embedded fold indicators
|
||||
- **Hex + ASCII columns** — raw byte previews alongside the structured view with per-byte change highlighting
|
||||
|
||||
## Roadmap
|
||||
### Live Memory Analysis
|
||||
|
||||
- [ ] Process memory section enumeration
|
||||
- [ ] Address parser auto-complete
|
||||
- [ ] Safe mode
|
||||
- [ ] File import for other Reclass instances
|
||||
- [ ] Expose UI functionality to plugins
|
||||
- [ ] iOS/macOS support
|
||||
- [ ] Display RTTI information
|
||||
- **Auto-refresh** — configurable interval (default 660ms) with async page-based reads for non-blocking UI
|
||||
- **Value history & heatmap** — per-node ring buffer (10 samples with timestamps), color-coded heat indicators (static/cold/warm/hot) based on change frequency
|
||||
- **Changed-byte highlighting** — per-byte change indicators within hex preview lines
|
||||
- **Memory write-back** — edit values inline, writes propagate through the provider to live process memory
|
||||
- **Pointer chasing** — automatic reads of dereferenced memory regions across pointer chains
|
||||
- **Address parser** — formula expressions like `<module.exe>+0x1A0`, pointer dereference chains, symbol resolution
|
||||
|
||||
### Undo / Redo
|
||||
|
||||
Full command stack with 15 undoable operations: ChangeKind, Rename, Collapse, Insert, Remove, ChangeBase, WriteBytes, ChangeArrayMeta, ChangePointerRef, ChangeStructTypeName, ChangeClassKeyword, ChangeOffset, ChangeEnumMembers, ChangeOffsetExpr, ToggleStatic. Batch macro support for multi-node operations.
|
||||
|
||||
### Import / Export
|
||||
|
||||
| Format | Import | Export |
|
||||
|--------|:------:|:------:|
|
||||
| **Native JSON (.rcx)** | Full tree + metadata | Full tree + metadata |
|
||||
| **C/C++ source** | Struct/class/union/enum parsing with offset comments | Header generation with optional static asserts |
|
||||
| **ReClass XML** | Full compatibility with ReClass Classic | Full compatibility |
|
||||
| **PDB symbols (Windows)** | UDT enumeration with selective recursive import via raw_pdb — no DIA SDK dependency | |
|
||||
|
||||
### Workspace & Navigation
|
||||
|
||||
- **Multi-document tabs** — MDI interface, one document per tab
|
||||
- **Workspace dock** — project explorer tree with struct/enum/union icons, sorted by field count, quick navigation to members
|
||||
- **Scanner dock** — integrated memory search panel
|
||||
- **Dual view mode** — switch between ReClass tree view and rendered C/C++ output per tab
|
||||
- **View root** — focus on a specific struct, hiding all others
|
||||
- **Scroll to node** — programmatic navigation to any node by ID
|
||||
|
||||
## Data Sources
|
||||
|
||||
- **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
|
||||
- **Remote Process** — read another process's memory via shared memory
|
||||
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
|
||||
- **Process** — attach to a live process and read its memory in real time (Windows/Linux)
|
||||
- **Kernel driver** — Windows kernel driver (IOCTL) for process memory, physical memory, page table walking, and CR3/VTOP translation
|
||||
- **Remote Process** — read another process's memory over TCP with cross-architecture 32/64-bit support
|
||||
- **WinDbg** — connect to live WinDbg debugging sessions or load crash dumps
|
||||
- **Saved sources** — quick-switch between recently used data sources per tab
|
||||
|
||||
## Screenshots
|
||||
## Plugin System
|
||||
|
||||

|
||||
DLL plugins loaded from a `Plugins` folder, auto or manual.
|
||||
|
||||

|
||||
**Bundled plugins:**
|
||||
|
||||

|
||||
| Plugin | Description |
|
||||
|--------|-------------|
|
||||
| **Process memory** | Attach to local processes on Windows and Linux — PID-based, with symbol resolution and module/region enumeration |
|
||||
| **Kernel memory** | Windows kernel driver (IOCTL) for reading/writing process and physical memory, CR3 queries, virtual-to-physical translation, and full 4-level page table walking — supports 4KB, 2MB, and 1GB pages |
|
||||
| **WinDbg** | Access data from live WinDbg debugging sessions |
|
||||
| **Remote process memory** | TCP RPC-based remote process access with cross-architecture support |
|
||||
| **ReClass.NET compatibility** | Load existing ReClass.NET native DLL plugins directly; optional .NET CLR hosting for managed plugins |
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge`. The server starts automatically on launch and can be toggled from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code). A standalone stdio-to-pipe bridge binary is built alongside the main application. To connect, add this to your MCP client config (e.g. `.mcp.json`):
|
||||
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge` — the first reverse engineering tool with native AI/LLM integration. The server uses JSON-RPC 2.0 over named pipes and can be toggled from the Tools menu or auto-started on launch.
|
||||
|
||||
**Available tools:**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `projectState` | Read current tree structure, base address, tab state |
|
||||
| `treeApply` | Apply structural command deltas to the node tree |
|
||||
| `sourceSwitch` | Switch the active data source |
|
||||
| `hexRead` | Read bytes at an address |
|
||||
| `hexWrite` | Write bytes at an address |
|
||||
| `statusSet` | Update the status bar text |
|
||||
| `uiAction` | Trigger menu actions programmatically |
|
||||
| `treeSearch` | Search nodes by name or type |
|
||||
| `nodeHistory` | Query value change history for a node |
|
||||
|
||||
**Notifications:** `notifyTreeChanged`, `notifyDataChanged`
|
||||
|
||||
A standalone stdio-to-pipe bridge binary is built alongside the main application. To connect, add this to your MCP client config (e.g. `.mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -101,6 +154,16 @@ cd Reclass
|
||||
|
||||
The build script auto-detects your Qt install location.
|
||||
|
||||
### macOS Build
|
||||
|
||||
```bash
|
||||
./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package
|
||||
```
|
||||
|
||||
If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`.
|
||||
|
||||
Note: macOS Gatekeeper may block unsigned apps. If the app won't open, go to **System Settings > Privacy & Security** and click **Open Anyway**.
|
||||
|
||||
### Manual Build (MinGW)
|
||||
|
||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||
@@ -122,6 +185,8 @@ The `msvc/` folder contains a ready-made solution (`Reclass.slnx`) with projects
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
30 tests covering composition, serialization, undo/redo, import/export, provider switching, type visibility, validation, scanning, and rendering.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# cmake/deploy.cmake - Dual-mode script for deploying Qt runtime DLLs
|
||||
#
|
||||
# Script mode: cmake -P deploy.cmake <target_exe> <windeployqt>
|
||||
# Include mode: include(deploy) from CMakeLists.txt (creates "deploy" target)
|
||||
# Include mode: include(deploy) from CMakeLists.txt (creates "deploy" target + post-build)
|
||||
|
||||
if(CMAKE_SCRIPT_MODE_FILE)
|
||||
set(TARGET_EXE ${CMAKE_ARGV3})
|
||||
@@ -17,7 +17,6 @@ if(CMAKE_SCRIPT_MODE_FILE)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${WINDEPLOYQT}
|
||||
--pdb
|
||||
--no-compiler-runtime
|
||||
--no-translations
|
||||
--no-opengl-sw
|
||||
@@ -67,6 +66,7 @@ if(NOT TARGET ${QT}::windeployqt AND TARGET ${QT}::qmake)
|
||||
endif()
|
||||
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
# Standalone "deploy" target (can still be invoked manually)
|
||||
add_custom_target(deploy
|
||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
||||
$<TARGET_FILE:Reclass>
|
||||
@@ -79,4 +79,13 @@ if(TARGET ${QT}::windeployqt)
|
||||
set_target_properties(deploy PROPERTIES
|
||||
ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:Reclass>/.qt_deployed
|
||||
)
|
||||
|
||||
# Auto-deploy as post-build step so the correct Qt DLLs are always next
|
||||
# to the exe. Without this, MSVC builds load whatever Qt DLLs happen to
|
||||
# be in PATH (often MinGW ones), causing instant ABI-mismatch crashes.
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
||||
$<TARGET_FILE:Reclass>
|
||||
$<TARGET_FILE:${QT}::windeployqt>
|
||||
COMMENT "Auto-deploying Qt runtime DLLs...")
|
||||
endif()
|
||||
|
||||
29
cmake/raw_pdb_prefix.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// Force-included before every raw_pdb translation unit (and consumers).
|
||||
// PDB_CRT.h forward-declares printf/memcmp/etc with extern "C" __cdecl,
|
||||
// which conflicts with MinGW's CRT headers (C++ linkage, no __cdecl).
|
||||
//
|
||||
// Fix: include the real CRT headers, then include PDB_CRT.h with function
|
||||
// names macro-renamed to harmless dummies. This triggers #pragma once so
|
||||
// no raw_pdb source file ever processes PDB_CRT.h's conflicting declarations.
|
||||
//
|
||||
// Guarded with __cplusplus because PUBLIC propagation applies this to C
|
||||
// sources (fadec) where PDB_CRT.h is irrelevant and <cstdio> doesn't exist.
|
||||
#ifdef __cplusplus
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#undef __cdecl
|
||||
#define __cdecl
|
||||
|
||||
#define printf _pdb_crt_unused_printf
|
||||
#define memcmp _pdb_crt_unused_memcmp
|
||||
#define memcpy _pdb_crt_unused_memcpy
|
||||
#define strlen _pdb_crt_unused_strlen
|
||||
#define strcmp _pdb_crt_unused_strcmp
|
||||
#include "Foundation/PDB_CRT.h"
|
||||
#undef printf
|
||||
#undef memcmp
|
||||
#undef memcpy
|
||||
#undef strlen
|
||||
#undef strcmp
|
||||
#endif
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 113 KiB |
BIN
docs/README_PIC4.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/README_PIC5.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
docs/README_PIC6.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
@@ -66,15 +66,28 @@
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
|
||||
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Link>
|
||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<PostBuildEvent>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
|
||||
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ClCompile>
|
||||
@@ -129,10 +142,12 @@
|
||||
<ClInclude Include="..\src\addressparser.h" />
|
||||
<ClInclude Include="..\src\core.h" />
|
||||
<ClInclude Include="..\src\disasm.h" />
|
||||
<QtMoc Include="..\src\dock_tab_buttons.h" />
|
||||
<ClInclude Include="..\src\generator.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\pluginmanager.h" />
|
||||
<ClInclude Include="..\src\providerregistry.h" />
|
||||
<QtMoc Include="..\src\startpage.h" />
|
||||
<ClInclude Include="..\src\workspace_model.h" />
|
||||
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
|
||||
<ClInclude Include="..\src\imports\import_pdb.h" />
|
||||
@@ -152,7 +167,12 @@
|
||||
<ClCompile Include="..\src\editor.cpp" />
|
||||
<ClCompile Include="..\src\format.cpp" />
|
||||
<ClCompile Include="..\src\generator.cpp" />
|
||||
<ClCompile Include="..\src\main.cpp" />
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">input</DynamicSource>
|
||||
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(Filename).moc</QtMocFileName>
|
||||
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
|
||||
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(Filename).moc</QtMocFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\optionsdialog.cpp" />
|
||||
<ClCompile Include="..\src\pluginmanager.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
|
||||
@@ -89,6 +89,12 @@
|
||||
<QtMoc Include="..\src\themes\thememanager.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\dock_tab_buttons.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\startpage.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\addressparser.h">
|
||||
@@ -165,9 +171,6 @@
|
||||
<ClCompile Include="..\src\generator.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\optionsdialog.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
@@ -219,5 +222,8 @@
|
||||
<ClCompile Include="..\src\themes\thememanager.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -45,3 +45,12 @@ set_target_properties(ProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
if(APPLE AND TARGET Reclass)
|
||||
add_custom_command(TARGET ProcessMemoryPlugin POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/../PlugIns"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:ProcessMemoryPlugin>"
|
||||
"$<TARGET_FILE_DIR:Reclass>/../PlugIns/"
|
||||
COMMENT "Copying ProcessMemoryPlugin into Reclass.app/Contents/PlugIns")
|
||||
endif()
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QImage>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMap>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
|
||||
#include <QtWin>
|
||||
#endif
|
||||
@@ -19,6 +20,60 @@
|
||||
#include <tlhelp32.h>
|
||||
#include <psapi.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
typedef struct _UNICODE_STRING { USHORT Length, MaximumLength; PWSTR Buffer; } UNICODE_STRING;
|
||||
typedef struct _CLIENT_ID { HANDLE UniqueProcess; HANDLE UniqueThread; } CLIENT_ID;
|
||||
typedef struct _SYSTEM_THREAD_INFORMATION {
|
||||
LARGE_INTEGER KernelTime, UserTime, CreateTime;
|
||||
ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientId;
|
||||
LONG Priority, BasePriority; ULONG ContextSwitches, ThreadState, WaitReason;
|
||||
} SYSTEM_THREAD_INFORMATION;
|
||||
typedef struct _SYSTEM_PROCESS_INFORMATION {
|
||||
ULONG NextEntryOffset; // 0x000
|
||||
ULONG NumberOfThreads; // 0x004
|
||||
LARGE_INTEGER WorkingSetPrivateSize; // 0x008
|
||||
ULONG HardFaultCount; // 0x010
|
||||
ULONG NumberOfThreadsHighWatermark; // 0x014
|
||||
ULONGLONG CycleTime; // 0x018
|
||||
LARGE_INTEGER CreateTime; // 0x020
|
||||
LARGE_INTEGER UserTime; // 0x028
|
||||
LARGE_INTEGER KernelTime; // 0x030
|
||||
UNICODE_STRING ImageName; // 0x038
|
||||
LONG BasePriority; // 0x048
|
||||
HANDLE UniqueProcessId; // 0x050
|
||||
PVOID InheritedFromUniqueProcessId; // 0x058
|
||||
ULONG HandleCount; // 0x060
|
||||
ULONG SessionId; // 0x064
|
||||
ULONG_PTR UniqueProcessKey; // 0x068
|
||||
SIZE_T PeakVirtualSize; // 0x070
|
||||
SIZE_T VirtualSize; // 0x078
|
||||
ULONG PageFaultCount; // 0x080
|
||||
ULONG _pad0; // 0x084
|
||||
SIZE_T PeakWorkingSetSize; // 0x088
|
||||
SIZE_T WorkingSetSize; // 0x090
|
||||
SIZE_T QuotaPeakPagedPoolUsage; // 0x098
|
||||
SIZE_T QuotaPagedPoolUsage; // 0x0A0
|
||||
SIZE_T QuotaPeakNonPagedPoolUsage; // 0x0A8
|
||||
SIZE_T QuotaNonPagedPoolUsage; // 0x0B0
|
||||
SIZE_T PagefileUsage; // 0x0B8
|
||||
SIZE_T PeakPagefileUsage; // 0x0C0
|
||||
SIZE_T PrivatePageCount; // 0x0C8
|
||||
LARGE_INTEGER ReadOperationCount; // 0x0D0
|
||||
LARGE_INTEGER WriteOperationCount; // 0x0D8
|
||||
LARGE_INTEGER OtherOperationCount; // 0x0E0
|
||||
LARGE_INTEGER ReadTransferCount; // 0x0E8
|
||||
LARGE_INTEGER WriteTransferCount; // 0x0F0
|
||||
LARGE_INTEGER OtherTransferCount; // 0x0F8
|
||||
} SYSTEM_PROCESS_INFORMATION; // sizeof = 0x100
|
||||
typedef struct alignas(8) _THREAD_BASIC_INFORMATION {
|
||||
NTSTATUS ExitStatus; // 0x00
|
||||
ULONG _pad; // 0x04
|
||||
PVOID TebBaseAddress; // 0x08
|
||||
CLIENT_ID ClientId; // 0x10
|
||||
ULONG_PTR AffinityMask; // 0x20
|
||||
LONG Priority; // 0x28
|
||||
LONG BasePriority; // 0x2C
|
||||
} THREAD_BASIC_INFORMATION;
|
||||
#elif defined(__linux__)
|
||||
#include <climits>
|
||||
#include <sys/types.h>
|
||||
@@ -29,6 +84,13 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
@@ -61,6 +123,17 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(m_handle, &isWow64) && isWow64)
|
||||
m_pointerSize = 4;
|
||||
// Query PEB address via NtQueryInformationProcess
|
||||
{
|
||||
typedef NTSTATUS(NTAPI* NtQIP_t)(HANDLE, ULONG, PVOID, ULONG, PULONG);
|
||||
static NtQIP_t pNtQIP = (NtQIP_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");
|
||||
if (pNtQIP) {
|
||||
struct { PVOID r1; PVOID PebBaseAddress; PVOID r2[2]; ULONG_PTR pid; PVOID r3; } pbi = {};
|
||||
ULONG retLen = 0;
|
||||
if (pNtQIP(m_handle, /*ProcessBasicInformation*/0, &pbi, sizeof(pbi), &retLen) >= 0 && pbi.PebBaseAddress)
|
||||
m_peb = (uint64_t)(uintptr_t)pbi.PebBaseAddress;
|
||||
}
|
||||
}
|
||||
cacheModules();
|
||||
}
|
||||
}
|
||||
@@ -120,8 +193,14 @@ void ProcessMemoryProvider::cacheModules()
|
||||
if ( i == 0 )
|
||||
m_base = (uint64_t)mi.lpBaseOfDll;
|
||||
|
||||
m_modules.append({
|
||||
WCHAR modPath[MAX_PATH];
|
||||
QString fullPath;
|
||||
if (GetModuleFileNameExW(m_handle, mods[i], modPath, MAX_PATH))
|
||||
fullPath = QString::fromWCharArray(modPath);
|
||||
|
||||
m_modules.push_back(ModuleInfo{
|
||||
QString::fromWCharArray(modName),
|
||||
fullPath,
|
||||
(uint64_t)mi.lpBaseOfDll,
|
||||
(uint64_t)mi.SizeOfImage
|
||||
});
|
||||
@@ -129,6 +208,15 @@ void ProcessMemoryProvider::cacheModules()
|
||||
}
|
||||
}
|
||||
|
||||
QVector<rcx::Provider::ModuleEntry> ProcessMemoryProvider::enumerateModules() const
|
||||
{
|
||||
QVector<ModuleEntry> result;
|
||||
result.reserve(m_modules.size());
|
||||
for (const auto& m : m_modules)
|
||||
result.push_back(ModuleEntry{m.name, m.fullPath, m.base, m.size});
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
@@ -335,8 +423,9 @@ void ProcessMemoryProvider::cacheModules()
|
||||
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
|
||||
{
|
||||
QFileInfo fi(it.key());
|
||||
m_modules.append({
|
||||
m_modules.push_back(ModuleInfo{
|
||||
fi.fileName(),
|
||||
it.key(),
|
||||
it->base,
|
||||
it->end - it->base
|
||||
});
|
||||
@@ -395,8 +484,239 @@ QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
return regions;
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_task(0)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
, m_writable(false)
|
||||
, m_base(0)
|
||||
{
|
||||
mach_port_t task = MACH_PORT_NULL;
|
||||
kern_return_t kr = task_for_pid(mach_task_self(), static_cast<int>(pid), &task);
|
||||
if (kr != KERN_SUCCESS || task == MACH_PORT_NULL)
|
||||
return;
|
||||
|
||||
m_task = static_cast<uint32_t>(task);
|
||||
m_writable = true;
|
||||
|
||||
proc_bsdinfo bsdInfo{};
|
||||
int infoLen = proc_pidinfo(static_cast<int>(pid), PROC_PIDTBSDINFO, 0, &bsdInfo, sizeof(bsdInfo));
|
||||
if (infoLen == (int)sizeof(bsdInfo)) {
|
||||
#ifdef PROC_FLAG_LP64
|
||||
m_pointerSize = (bsdInfo.pbi_flags & PROC_FLAG_LP64) ? 8 : 4;
|
||||
#else
|
||||
m_pointerSize = 8;
|
||||
#endif
|
||||
}
|
||||
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (m_task == 0 || len <= 0)
|
||||
return false;
|
||||
|
||||
mach_vm_size_t outSize = 0;
|
||||
kern_return_t kr = mach_vm_read_overwrite(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
static_cast<mach_vm_address_t>(addr),
|
||||
static_cast<mach_vm_size_t>(len),
|
||||
reinterpret_cast<mach_vm_address_t>(buf),
|
||||
&outSize);
|
||||
|
||||
if ((int)outSize < len)
|
||||
memset((char*)buf + outSize, 0, len - outSize);
|
||||
|
||||
return kr == KERN_SUCCESS && outSize > 0;
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (m_task == 0 || !m_writable || len <= 0)
|
||||
return false;
|
||||
|
||||
kern_return_t kr = mach_vm_write(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
static_cast<mach_vm_address_t>(addr),
|
||||
reinterpret_cast<vm_offset_t>(const_cast<void*>(buf)),
|
||||
static_cast<mach_msg_type_number_t>(len));
|
||||
return kr == KERN_SUCCESS;
|
||||
}
|
||||
|
||||
QString ProcessMemoryProvider::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 {};
|
||||
}
|
||||
|
||||
void ProcessMemoryProvider::cacheModules()
|
||||
{
|
||||
if (m_task == 0)
|
||||
return;
|
||||
|
||||
m_modules.clear();
|
||||
|
||||
char mainPathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
QString mainPath;
|
||||
if (proc_pidpath((int)m_pid, mainPathBuf, sizeof(mainPathBuf)) > 0)
|
||||
mainPath = QString::fromUtf8(mainPathBuf);
|
||||
|
||||
struct Range { uint64_t base; uint64_t end; };
|
||||
QMap<QString, Range> moduleRanges;
|
||||
|
||||
mach_vm_address_t addr = 0;
|
||||
uint32_t depth = 0;
|
||||
for (;;) {
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_submap_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
|
||||
kern_return_t kr = mach_vm_region_recurse(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
&addr,
|
||||
&size,
|
||||
&depth,
|
||||
reinterpret_cast<vm_region_recurse_info_t>(&info),
|
||||
&count);
|
||||
if (kr != KERN_SUCCESS)
|
||||
break;
|
||||
|
||||
if (info.is_submap) {
|
||||
++depth;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
++addr;
|
||||
continue;
|
||||
}
|
||||
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int pathLen = proc_regionfilename((int)m_pid, (uint64_t)addr, pathBuf, sizeof(pathBuf));
|
||||
if (pathLen > 0) {
|
||||
QString fullPath = QString::fromUtf8(pathBuf);
|
||||
|
||||
uint64_t regionBase = (uint64_t)addr;
|
||||
uint64_t regionEnd = regionBase + (uint64_t)size;
|
||||
auto it = moduleRanges.find(fullPath);
|
||||
if (it == moduleRanges.end()) {
|
||||
moduleRanges.insert(fullPath, {regionBase, regionEnd});
|
||||
} else {
|
||||
if (regionBase < it->base) it->base = regionBase;
|
||||
if (regionEnd > it->end) it->end = regionEnd;
|
||||
}
|
||||
|
||||
if (m_base == 0 && !mainPath.isEmpty() && fullPath == mainPath && (info.protection & VM_PROT_EXECUTE))
|
||||
m_base = regionBase;
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)addr + (uint64_t)size;
|
||||
if (next <= (uint64_t)addr)
|
||||
break;
|
||||
addr = (mach_vm_address_t)next;
|
||||
}
|
||||
|
||||
m_modules.reserve(moduleRanges.size());
|
||||
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
|
||||
{
|
||||
QFileInfo fi(it.key());
|
||||
m_modules.push_back(ModuleInfo{
|
||||
fi.fileName(),
|
||||
it.key(),
|
||||
it->base,
|
||||
it->end - it->base
|
||||
});
|
||||
}
|
||||
|
||||
if (m_base == 0 && !m_modules.isEmpty())
|
||||
m_base = m_modules.front().base;
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
if (m_task == 0)
|
||||
return regions;
|
||||
|
||||
mach_vm_address_t addr = 0;
|
||||
uint32_t depth = 0;
|
||||
for (;;) {
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_submap_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
|
||||
kern_return_t kr = mach_vm_region_recurse(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
&addr,
|
||||
&size,
|
||||
&depth,
|
||||
reinterpret_cast<vm_region_recurse_info_t>(&info),
|
||||
&count);
|
||||
if (kr != KERN_SUCCESS)
|
||||
break;
|
||||
|
||||
if (info.is_submap) {
|
||||
++depth;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
++addr;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool readable = (info.protection & VM_PROT_READ) != 0;
|
||||
if (readable)
|
||||
{
|
||||
rcx::MemoryRegion region;
|
||||
region.base = (uint64_t)addr;
|
||||
region.size = (uint64_t)size;
|
||||
region.readable = readable;
|
||||
region.writable = (info.protection & VM_PROT_WRITE) != 0;
|
||||
region.executable = (info.protection & VM_PROT_EXECUTE) != 0;
|
||||
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int pathLen = proc_regionfilename((int)m_pid, region.base, pathBuf, sizeof(pathBuf));
|
||||
if (pathLen > 0) {
|
||||
QFileInfo fi(QString::fromUtf8(pathBuf));
|
||||
region.moduleName = fi.fileName();
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)addr + (uint64_t)size;
|
||||
if (next <= (uint64_t)addr)
|
||||
break;
|
||||
addr = (mach_vm_address_t)next;
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
#endif // platform
|
||||
|
||||
#ifndef _WIN32
|
||||
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;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
@@ -414,6 +734,9 @@ ProcessMemoryProvider::~ProcessMemoryProvider()
|
||||
#elif defined(__linux__)
|
||||
if (m_fd >= 0)
|
||||
::close(m_fd);
|
||||
#elif defined(__APPLE__)
|
||||
if (m_task != 0)
|
||||
mach_port_deallocate(mach_task_self(), static_cast<mach_port_name_t>(m_task));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -423,9 +746,63 @@ int ProcessMemoryProvider::size() const
|
||||
return m_handle ? 0x10000 : 0;
|
||||
#elif defined(__linux__)
|
||||
return (m_fd >= 0) ? 0x10000 : 0;
|
||||
#elif defined(__APPLE__)
|
||||
return (m_task != 0) ? 0x10000 : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
QVector<rcx::Provider::ThreadInfo> ProcessMemoryProvider::tebs() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
QVector<ThreadInfo> result;
|
||||
if (!m_handle || !m_peb) return result;
|
||||
|
||||
typedef NTSTATUS(NTAPI* NtQSI_t)(ULONG, PVOID, ULONG, PULONG);
|
||||
typedef NTSTATUS(NTAPI* NtQIT_t)(HANDLE, ULONG, PVOID, ULONG, PULONG);
|
||||
static auto pNtQSI = (NtQSI_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
|
||||
static auto pNtQIT = (NtQIT_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationThread");
|
||||
if (!pNtQSI || !pNtQIT) return result;
|
||||
|
||||
// Enumerate threads via SystemProcessInformation (class 5)
|
||||
ULONG retLen = 0;
|
||||
ULONG bufSize = 1 << 20;
|
||||
QByteArray buf(bufSize, 0);
|
||||
NTSTATUS qsiSt;
|
||||
for (int attempt = 0; attempt < 8; ++attempt) {
|
||||
qsiSt = pNtQSI(5, buf.data(), bufSize, &retLen);
|
||||
if ((uint32_t)qsiSt != 0xC0000004u) break;
|
||||
bufSize *= 2;
|
||||
buf.resize(bufSize);
|
||||
}
|
||||
if (qsiSt < 0) return result;
|
||||
|
||||
// Walk process entries to find ours
|
||||
auto* proc = (SYSTEM_PROCESS_INFORMATION*)buf.data();
|
||||
for (;;) {
|
||||
if ((uintptr_t)proc->UniqueProcessId == m_pid) {
|
||||
auto* threads = (SYSTEM_THREAD_INFORMATION*)((char*)proc + sizeof(*proc));
|
||||
for (ULONG i = 0; i < proc->NumberOfThreads; ++i) {
|
||||
DWORD tid = (DWORD)(uintptr_t)threads[i].ClientId.UniqueThread;
|
||||
HANDLE hThread = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, tid);
|
||||
if (!hThread) continue;
|
||||
THREAD_BASIC_INFORMATION tbi = {};
|
||||
ULONG tbiLen = 0;
|
||||
NTSTATUS qitSt = pNtQIT(hThread, 0, &tbi, sizeof(tbi), &tbiLen);
|
||||
if (qitSt >= 0 && tbi.TebBaseAddress)
|
||||
result.push_back(ThreadInfo{(uint64_t)(uintptr_t)tbi.TebBaseAddress, tid});
|
||||
CloseHandle(hThread);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!proc->NextEntryOffset) break;
|
||||
proc = (SYSTEM_PROCESS_INFORMATION*)((char*)proc + proc->NextEntryOffset);
|
||||
}
|
||||
return result;
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
@@ -521,6 +898,68 @@ uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
#elif defined(__APPLE__)
|
||||
QStringList parts = target.split(':');
|
||||
bool ok = false;
|
||||
uint32_t pid = parts[0].toUInt(&ok);
|
||||
if (!ok || pid == 0)
|
||||
return 0;
|
||||
|
||||
mach_port_t task = MACH_PORT_NULL;
|
||||
kern_return_t tkr = task_for_pid(mach_task_self(), static_cast<int>(pid), &task);
|
||||
if (tkr != KERN_SUCCESS || task == MACH_PORT_NULL)
|
||||
return 0;
|
||||
|
||||
char mainPathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
QString mainPath;
|
||||
if (proc_pidpath((int)pid, mainPathBuf, sizeof(mainPathBuf)) > 0)
|
||||
mainPath = QString::fromUtf8(mainPathBuf);
|
||||
|
||||
uint64_t base = 0;
|
||||
mach_vm_address_t addr = 0;
|
||||
uint32_t depth = 0;
|
||||
for (;;) {
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_submap_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
|
||||
kern_return_t kr = mach_vm_region_recurse(task, &addr, &size, &depth,
|
||||
reinterpret_cast<vm_region_recurse_info_t>(&info),
|
||||
&count);
|
||||
if (kr != KERN_SUCCESS)
|
||||
break;
|
||||
|
||||
if (info.is_submap) {
|
||||
++depth;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
++addr;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((info.protection & VM_PROT_EXECUTE) != 0) {
|
||||
if (!mainPath.isEmpty()) {
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int pathLen = proc_regionfilename((int)pid, (uint64_t)addr, pathBuf, sizeof(pathBuf));
|
||||
if (pathLen > 0 && QString::fromUtf8(pathBuf) == mainPath) {
|
||||
base = (uint64_t)addr;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
base = (uint64_t)addr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)addr + (uint64_t)size;
|
||||
if (next <= (uint64_t)addr)
|
||||
break;
|
||||
addr = (mach_vm_address_t)next;
|
||||
}
|
||||
|
||||
mach_port_deallocate(mach_task_self(), task);
|
||||
return base;
|
||||
#else
|
||||
Q_UNUSED(target);
|
||||
return 0;
|
||||
@@ -664,6 +1103,61 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
::close(exeFd);
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
QIcon defaultIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
|
||||
int bytes = proc_listpids(PROC_ALL_PIDS, 0, nullptr, 0);
|
||||
if (bytes <= 0)
|
||||
return processes;
|
||||
|
||||
int count = bytes / (int)sizeof(pid_t);
|
||||
QVector<pid_t> pids(count);
|
||||
bytes = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), count * (int)sizeof(pid_t));
|
||||
if (bytes <= 0)
|
||||
return processes;
|
||||
|
||||
count = bytes / (int)sizeof(pid_t);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
pid_t pid = pids[i];
|
||||
if (pid <= 0)
|
||||
continue;
|
||||
|
||||
mach_port_t task = MACH_PORT_NULL;
|
||||
if (task_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS || task == MACH_PORT_NULL)
|
||||
continue;
|
||||
mach_port_deallocate(mach_task_self(), task);
|
||||
|
||||
char nameBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int nameLen = proc_name(pid, nameBuf, sizeof(nameBuf));
|
||||
QString procName;
|
||||
if (nameLen > 0)
|
||||
procName = QString::fromUtf8(nameBuf);
|
||||
if (procName.isEmpty())
|
||||
continue;
|
||||
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
QString procPath;
|
||||
if (proc_pidpath(pid, pathBuf, sizeof(pathBuf)) > 0)
|
||||
procPath = QString::fromUtf8(pathBuf);
|
||||
|
||||
PluginProcessInfo info;
|
||||
info.pid = static_cast<uint32_t>(pid);
|
||||
info.name = procName;
|
||||
info.path = procPath;
|
||||
info.icon = defaultIcon;
|
||||
|
||||
proc_bsdinfo bsdInfo{};
|
||||
int infoLen = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdInfo, sizeof(bsdInfo));
|
||||
if (infoLen == (int)sizeof(bsdInfo)) {
|
||||
#ifdef PROC_FLAG_LP64
|
||||
info.is32Bit = (bsdInfo.pbi_flags & PROC_FLAG_LP64) == 0;
|
||||
#else
|
||||
info.is32Bit = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -35,12 +35,17 @@ public:
|
||||
return m_handle && len >= 0;
|
||||
#elif defined(__linux__)
|
||||
return m_fd >= 0 && len >= 0;
|
||||
#elif defined(__APPLE__)
|
||||
return m_task != 0 && len >= 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Process-specific helpers
|
||||
uint32_t pid() const { return m_pid; }
|
||||
void refreshModules() { m_modules.clear(); cacheModules(); }
|
||||
uint64_t peb() const override { return m_peb; }
|
||||
QVector<ThreadInfo> tebs() const override;
|
||||
QVector<ModuleEntry> enumerateModules() const override;
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
@@ -50,15 +55,19 @@ private:
|
||||
void* m_handle;
|
||||
#elif defined(__linux__)
|
||||
int m_fd;
|
||||
#elif defined(__APPLE__)
|
||||
uint32_t m_task;
|
||||
#endif
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
bool m_writable;
|
||||
uint64_t m_base;
|
||||
int m_pointerSize = 8;
|
||||
uint64_t m_peb = 0;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
QString fullPath;
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
};
|
||||
|
||||
@@ -244,7 +244,7 @@ struct IpcClient {
|
||||
reinterpret_cast<const char*>(data + entry->nameOffset),
|
||||
(int)entry->nameLength);
|
||||
#endif
|
||||
result.append({modName, entry->base, entry->size});
|
||||
result.push_back(RemoteProcessProvider::ModuleInfo{modName, entry->base, entry->size});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ set(PLUGIN_SOURCES
|
||||
add_library(WinDbgMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
# Link Qt + DbgEng
|
||||
target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets dbgeng ole32)
|
||||
target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets ole32)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(WinDbgMemoryPlugin PRIVATE
|
||||
|
||||
@@ -12,12 +12,99 @@
|
||||
#include <QDebug>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
#pragma comment(lib, "dbgeng.lib")
|
||||
// dbgeng.dll is loaded dynamically — see loadDbgEngTools()
|
||||
|
||||
// The system dbgeng.dll (C:\Windows\System32) does not support remote
|
||||
// connections (DebugConnect returns 0x8007053d). The full version lives
|
||||
// in the Debugging Tools for Windows directory. We load it dynamically
|
||||
// so the plugin works without requiring the debugger tools on PATH.
|
||||
static const char* const kDbgToolsDirs[] = {
|
||||
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64",
|
||||
"C:\\Program Files\\Windows Kits\\10\\Debuggers\\x64",
|
||||
};
|
||||
static const char* const kSettingsKey = "WinDbgPlugin/DbgToolsDir";
|
||||
|
||||
typedef HRESULT (STDAPICALLTYPE *PFN_DebugConnect)(PCSTR, REFIID, PVOID*);
|
||||
typedef HRESULT (STDAPICALLTYPE *PFN_DebugCreate)(REFIID, PVOID*);
|
||||
|
||||
static QString s_loadedDir;
|
||||
static HMODULE s_hDbgEng = nullptr;
|
||||
|
||||
static HMODULE tryLoadFrom(const char* dir) {
|
||||
SetDllDirectoryA(dir);
|
||||
// Pre-load dependencies so the tools versions are used instead of
|
||||
// the older System32 copies (e.g. dbghelp.dll without StackWalk2).
|
||||
char path[MAX_PATH];
|
||||
for (auto dep : {"dbghelp.dll", "dbgcore.dll", "symsrv.dll"}) {
|
||||
snprintf(path, sizeof(path), "%s\\%s", dir, dep);
|
||||
LoadLibraryA(path); // OK if missing
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s\\dbgeng.dll", dir);
|
||||
HMODULE h = LoadLibraryA(path);
|
||||
if (h) {
|
||||
s_loadedDir = QString::fromLocal8Bit(dir);
|
||||
qDebug() << "[WinDbg] Loaded dbgeng.dll from" << dir;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static HMODULE loadDbgEngTools() {
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
|
||||
// 1. Try user-configured path from settings
|
||||
QSettings settings;
|
||||
QString userDir = settings.value(kSettingsKey).toString();
|
||||
if (!userDir.isEmpty()) {
|
||||
s_hDbgEng = tryLoadFrom(userDir.toLocal8Bit().constData());
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
}
|
||||
|
||||
// 2. Try well-known install paths
|
||||
for (auto dir : kDbgToolsDirs) {
|
||||
s_hDbgEng = tryLoadFrom(dir);
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
}
|
||||
|
||||
SetDllDirectoryA(nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool dbgToolsFound() {
|
||||
loadDbgEngTools();
|
||||
return s_hDbgEng != nullptr;
|
||||
}
|
||||
|
||||
static PFN_DebugConnect getDebugConnect() {
|
||||
static PFN_DebugConnect pfn = nullptr;
|
||||
static bool tried = false;
|
||||
if (!tried) {
|
||||
tried = true;
|
||||
HMODULE h = loadDbgEngTools();
|
||||
if (h) pfn = (PFN_DebugConnect)GetProcAddress(h, "DebugConnect");
|
||||
if (!pfn) qWarning() << "[WinDbg] DebugConnect not available — Debugging Tools not found";
|
||||
}
|
||||
return pfn;
|
||||
}
|
||||
|
||||
static PFN_DebugCreate getDebugCreate() {
|
||||
static PFN_DebugCreate pfn = nullptr;
|
||||
static bool tried = false;
|
||||
if (!tried) {
|
||||
tried = true;
|
||||
HMODULE h = loadDbgEngTools();
|
||||
if (h) pfn = (PFN_DebugCreate)GetProcAddress(h, "DebugCreate");
|
||||
if (!pfn) qWarning() << "[WinDbg] DebugCreate not available — Debugging Tools not found";
|
||||
}
|
||||
return pfn;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
@@ -65,6 +152,9 @@ WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||
dispatchToOwner([this, &target]() {
|
||||
HRESULT hr;
|
||||
|
||||
// COM must be initialized on this thread for DbgEng
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
|
||||
qDebug() << "[WinDbg] Opening target:" << target
|
||||
<< "on DbgEng thread" << QThread::currentThread();
|
||||
|
||||
@@ -72,9 +162,11 @@ WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||
|| target.startsWith("npipe:", Qt::CaseInsensitive))
|
||||
{
|
||||
// ── Remote: connect to existing WinDbg debug server ──
|
||||
auto pfnConnect = getDebugConnect();
|
||||
if (!pfnConnect) { qWarning() << "[WinDbg] Debugging Tools required for remote connections"; return; }
|
||||
QByteArray connUtf8 = target.toUtf8();
|
||||
qDebug() << "[WinDbg] DebugConnect:" << target;
|
||||
hr = DebugConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client);
|
||||
hr = pfnConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client);
|
||||
qDebug() << "[WinDbg] DebugConnect hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "client=" << (void*)m_client;
|
||||
if (FAILED(hr) || !m_client) {
|
||||
@@ -86,7 +178,9 @@ WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||
else
|
||||
{
|
||||
// ── Local: create debug client for pid/dump ──
|
||||
hr = DebugCreate(IID_IDebugClient, (void**)&m_client);
|
||||
auto pfnCreate = getDebugCreate();
|
||||
if (!pfnCreate) { qWarning() << "[WinDbg] Debugging Tools required"; return; }
|
||||
hr = pfnCreate(IID_IDebugClient, (void**)&m_client);
|
||||
qDebug() << "[WinDbg] DebugCreate hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "client=" << (void*)m_client;
|
||||
if (FAILED(hr) || !m_client) {
|
||||
@@ -239,6 +333,7 @@ WinDbgMemoryProvider::~WinDbgMemoryProvider()
|
||||
m_client->DetachProcesses();
|
||||
}
|
||||
cleanup();
|
||||
CoUninitialize();
|
||||
});
|
||||
} else {
|
||||
// Thread not running — clean up directly (best-effort)
|
||||
@@ -503,7 +598,7 @@ std::unique_ptr<rcx::Provider> WinDbgMemoryPlugin::createProvider(const QString&
|
||||
*errorMsg = QString("Failed to connect to debug server.\n\n"
|
||||
"Target: %1\n\n"
|
||||
"Make sure WinDbg is running with a matching .server command\n"
|
||||
"(e.g. .server tcp:port=5055) and the port/pipe is reachable.")
|
||||
"(e.g. .server tcp:port=5056) and the port/pipe is reachable.")
|
||||
.arg(target);
|
||||
else if (target.startsWith("pid:", Qt::CaseInsensitive))
|
||||
*errorMsg = QString("Failed to attach to process.\n\n"
|
||||
@@ -532,7 +627,7 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
QDialog dlg(parent);
|
||||
dlg.setWindowTitle("WinDbg Settings");
|
||||
dlg.resize(460, 300);
|
||||
dlg.resize(480, 360);
|
||||
|
||||
QPalette dlgPal = qApp->palette();
|
||||
dlg.setPalette(dlgPal);
|
||||
@@ -540,17 +635,27 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
|
||||
auto* layout = new QVBoxLayout(&dlg);
|
||||
|
||||
QColor editBg = dlgPal.window().color().darker(115);
|
||||
QString editSS = QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||
" border-radius: 3px; padding: 4px 6px; }")
|
||||
.arg(editBg.name(),
|
||||
dlgPal.color(QPalette::Text).name(),
|
||||
dlgPal.color(QPalette::Mid).name());
|
||||
|
||||
layout->addWidget(new QLabel(
|
||||
"Connect to a running WinDbg debug server.\n"
|
||||
"In WinDbg, run: .server tcp:port=5055\n\n"
|
||||
"In WinDbg, run: .server tcp:port=5056\n\n"
|
||||
"Non-invasive debug and dump files only.\n"
|
||||
"Execution control (bp, g, t, p) is not supported."));
|
||||
"Execution control (bp, g, t, p) is not supported.\n"
|
||||
"WinDbg Classic is recommended."));
|
||||
|
||||
layout->addSpacing(8);
|
||||
layout->addWidget(new QLabel("Connection string:"));
|
||||
auto* connEdit = new QLineEdit;
|
||||
connEdit->setPlaceholderText("tcp:Port=5055,Server=localhost");
|
||||
connEdit->setText("tcp:Port=5055,Server=localhost");
|
||||
connEdit->setPlaceholderText("tcp:Port=5056,Server=127.0.0.1");
|
||||
connEdit->setText("tcp:Port=5056,Server=127.0.0.1");
|
||||
connEdit->setStyleSheet(editSS);
|
||||
layout->addWidget(connEdit);
|
||||
|
||||
layout->addSpacing(4);
|
||||
@@ -574,8 +679,72 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
layout->addLayout(row);
|
||||
};
|
||||
|
||||
addExample(".server tcp:port=5055");
|
||||
addExample(".server tcp:port=5056");
|
||||
addExample(".server npipe:pipe=reclass");
|
||||
|
||||
// ── Debugger Tools status ──
|
||||
layout->addSpacing(8);
|
||||
#ifdef _WIN32
|
||||
bool found = dbgToolsFound();
|
||||
auto* toolsRow = new QHBoxLayout;
|
||||
auto* toolsLabel = new QLabel;
|
||||
if (found) {
|
||||
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
toolsLabel->setPalette(tp);
|
||||
} else {
|
||||
toolsLabel->setText("Debugging Tools: not found");
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, QColor(220, 120, 80));
|
||||
toolsLabel->setPalette(tp);
|
||||
}
|
||||
toolsLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
toolsRow->addWidget(toolsLabel, 1);
|
||||
|
||||
auto* browseBtn = new QPushButton("Browse...");
|
||||
browseBtn->setFixedWidth(70);
|
||||
browseBtn->setToolTip("Locate Debugging Tools for Windows directory (contains dbgeng.dll)");
|
||||
QObject::connect(browseBtn, &QPushButton::clicked, [&dlg, toolsLabel, &dlgPal]() {
|
||||
QString dir = QFileDialog::getExistingDirectory(&dlg,
|
||||
"Locate Debugging Tools for Windows",
|
||||
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers");
|
||||
if (dir.isEmpty()) return;
|
||||
QString dllPath = dir + "/dbgeng.dll";
|
||||
if (!QFileInfo::exists(dllPath)) {
|
||||
QMessageBox::warning(&dlg, "Not Found",
|
||||
"dbgeng.dll was not found in that directory.\n"
|
||||
"Select the folder containing dbgeng.dll\n"
|
||||
"(e.g. Debuggers\\x64).");
|
||||
return;
|
||||
}
|
||||
QSettings settings;
|
||||
settings.setValue(kSettingsKey, dir);
|
||||
// Force reload on next use
|
||||
s_hDbgEng = nullptr;
|
||||
s_loadedDir.clear();
|
||||
if (dbgToolsFound()) {
|
||||
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
toolsLabel->setPalette(tp);
|
||||
}
|
||||
});
|
||||
toolsRow->addWidget(browseBtn);
|
||||
layout->addLayout(toolsRow);
|
||||
|
||||
if (!found) {
|
||||
auto* note = new QLabel(
|
||||
"The system dbgeng.dll does not support remote connections.\n"
|
||||
"Install Debugging Tools for Windows or use Browse to locate them.");
|
||||
QPalette np = dlgPal;
|
||||
np.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
note->setPalette(np);
|
||||
note->setWordWrap(true);
|
||||
layout->addWidget(note);
|
||||
}
|
||||
#endif
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
auto* btnLayout = new QHBoxLayout;
|
||||
|
||||
@@ -283,9 +283,10 @@ function Find-MinGWDirectory {
|
||||
$toolsDir = Join-Path $qtRoot "Tools"
|
||||
|
||||
if (Test-Path $toolsDir) {
|
||||
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
|
||||
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -match 'mingw'
|
||||
}
|
||||
$_.Name -match '^mingw\d+_\d+$'
|
||||
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
|
||||
|
||||
foreach ($dir in $mingwToolDirs) {
|
||||
$testBin = Join-Path $dir.FullName "bin\g++.exe"
|
||||
|
||||
168
scripts/build_macos.sh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
print_help() {
|
||||
cat <<'EOF'
|
||||
Reclass macOS Build Script
|
||||
|
||||
Usage:
|
||||
./scripts/build_macos.sh [options]
|
||||
|
||||
Options:
|
||||
--qt-dir <path> Qt installation prefix (e.g. /opt/homebrew/opt/qt)
|
||||
--build-type <type> Release | Debug | RelWithDebInfo | MinSizeRel (default: Release)
|
||||
--build-dir <path> Build directory (default: <repo>/build)
|
||||
--generator <name> CMake generator (default: Ninja if available)
|
||||
--clean Remove build directory before configuring
|
||||
--rebuild Clean then build
|
||||
--package Run macdeployqt and create a zip
|
||||
--tests Run ctest after build
|
||||
-h, --help Show this help
|
||||
|
||||
Notes:
|
||||
- You can set QTDIR or Qt6_DIR in your environment instead of --qt-dir.
|
||||
- If Qt is installed via Homebrew, the script will try to detect it.
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
project_root="$(cd "${script_dir}/.." && pwd)"
|
||||
|
||||
qt_dir=""
|
||||
build_type="Release"
|
||||
build_dir="${project_root}/build"
|
||||
generator=""
|
||||
do_clean="false"
|
||||
do_package="false"
|
||||
do_tests="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--qt-dir)
|
||||
qt_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-type)
|
||||
build_type="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-dir)
|
||||
build_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--generator)
|
||||
generator="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--clean)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--rebuild)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--package)
|
||||
do_package="true"
|
||||
shift
|
||||
;;
|
||||
--tests)
|
||||
do_tests="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
print_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
print_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${qt_dir}" ]]; then
|
||||
if [[ -n "${QTDIR:-}" ]]; then
|
||||
qt_dir="${QTDIR}"
|
||||
elif [[ -n "${Qt6_DIR:-}" ]]; then
|
||||
qt_dir="${Qt6_DIR}"
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
if brew --prefix qt >/dev/null 2>&1; then
|
||||
qt_dir="$(brew --prefix qt)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
echo "ERROR: cmake not found. Install CMake and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${generator}" ]]; then
|
||||
if command -v ninja >/dev/null 2>&1; then
|
||||
generator="Ninja"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${do_clean}" == "true" && -d "${build_dir}" ]]; then
|
||||
echo "Cleaning build directory: ${build_dir}"
|
||||
rm -rf "${build_dir}"
|
||||
fi
|
||||
|
||||
mkdir -p "${build_dir}"
|
||||
|
||||
cmake_args=(
|
||||
-S "${project_root}"
|
||||
-B "${build_dir}"
|
||||
-DCMAKE_BUILD_TYPE="${build_type}"
|
||||
)
|
||||
|
||||
if [[ -n "${generator}" ]]; then
|
||||
cmake_args+=(-G "${generator}")
|
||||
fi
|
||||
|
||||
if [[ -n "${qt_dir}" ]]; then
|
||||
export PATH="${qt_dir}/bin:${PATH}"
|
||||
cmake_args+=(-DCMAKE_PREFIX_PATH="${qt_dir}")
|
||||
fi
|
||||
|
||||
echo "Configuring..."
|
||||
cmake "${cmake_args[@]}"
|
||||
|
||||
echo "Building..."
|
||||
cmake --build "${build_dir}" --config "${build_type}"
|
||||
|
||||
if [[ "${do_tests}" == "true" ]]; then
|
||||
echo "Running tests..."
|
||||
ctest --test-dir "${build_dir}" --output-on-failure -C "${build_type}"
|
||||
fi
|
||||
|
||||
if [[ "${do_package}" == "true" ]]; then
|
||||
app_path="${build_dir}/Reclass.app"
|
||||
if [[ ! -d "${app_path}" ]]; then
|
||||
echo "ERROR: ${app_path} not found. Build may have failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
macdeployqt_bin=""
|
||||
if [[ -n "${qt_dir}" && -x "${qt_dir}/bin/macdeployqt" ]]; then
|
||||
macdeployqt_bin="${qt_dir}/bin/macdeployqt"
|
||||
elif command -v macdeployqt >/dev/null 2>&1; then
|
||||
macdeployqt_bin="$(command -v macdeployqt)"
|
||||
fi
|
||||
|
||||
if [[ -z "${macdeployqt_bin}" ]]; then
|
||||
echo "ERROR: macdeployqt not found. Ensure Qt is installed and in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running macdeployqt..."
|
||||
"${macdeployqt_bin}" "${app_path}" -always-overwrite
|
||||
|
||||
arch="$(uname -m)"
|
||||
zip_name="Reclass-macos-${arch}-qt6.zip"
|
||||
echo "Creating zip: ${zip_name}"
|
||||
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${build_dir}/${zip_name}"
|
||||
echo "Packaged: ${build_dir}/${zip_name}"
|
||||
fi
|
||||
@@ -318,10 +318,10 @@ $qtRoot = Split-Path (Split-Path $selectedQtDir -Parent) -Parent
|
||||
$toolsDir = Join-Path $qtRoot "Tools"
|
||||
|
||||
if (Test-Path $toolsDir) {
|
||||
# Look for MinGW tools directory
|
||||
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
|
||||
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -match 'mingw'
|
||||
}
|
||||
$_.Name -match '^mingw\d+_\d+$'
|
||||
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
|
||||
|
||||
foreach ($dir in $mingwToolDirs) {
|
||||
$testBin = Join-Path $dir.FullName "bin\g++.exe"
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace rcx {
|
||||
//
|
||||
// All numeric literals are hexadecimal (base 16).
|
||||
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
|
||||
// Module names with extensions (e.g. "client.dll") are scanned as one token.
|
||||
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
|
||||
|
||||
class ExpressionParser {
|
||||
@@ -273,16 +274,46 @@ private:
|
||||
// Identifier or hex literal disambiguation.
|
||||
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
|
||||
// Otherwise → backtrack and parse as hex number.
|
||||
// WinDbg-style "module!symbol" is scanned as a single identifier token.
|
||||
// If the identifier is followed by '(', try to parse as a built-in function call.
|
||||
bool parseIdentifierOrHex(uint64_t& result) {
|
||||
int start = m_pos;
|
||||
bool hasNonHex = false;
|
||||
|
||||
// Scan full token
|
||||
// Scan full token, including "module!symbol" as one token
|
||||
while (!atEnd() && isIdentChar(peek())) {
|
||||
if (!isHexDigit(peek()))
|
||||
hasNonHex = true;
|
||||
advance();
|
||||
}
|
||||
// Handle module.dll / module.exe / module.sys extensions
|
||||
// e.g. "client.dll + 0xFF" should parse "client.dll" as one token
|
||||
if (!atEnd() && peek() == '.' && m_pos > start) {
|
||||
int dotPos = m_pos;
|
||||
advance(); // skip '.'
|
||||
int extStart = m_pos;
|
||||
while (!atEnd() && isIdentChar(peek()))
|
||||
advance();
|
||||
if (m_pos > extStart) {
|
||||
hasNonHex = true; // '.' makes it definitively an identifier
|
||||
} else {
|
||||
m_pos = dotPos; // backtrack — '.' at end isn't an extension
|
||||
}
|
||||
}
|
||||
// If we hit '!' and the next char is an identifier start, extend the token
|
||||
// to include the second part (WinDbg module!symbol syntax)
|
||||
if (!atEnd() && peek() == '!' && m_pos > start) {
|
||||
int bangPos = m_pos;
|
||||
advance(); // skip '!'
|
||||
if (!atEnd() && isIdentStart(peek())) {
|
||||
hasNonHex = true;
|
||||
while (!atEnd() && isIdentChar(peek())) {
|
||||
advance();
|
||||
}
|
||||
} else {
|
||||
m_pos = bangPos; // backtrack — '!' at end isn't module!symbol
|
||||
}
|
||||
}
|
||||
|
||||
QString token = m_input.mid(start, m_pos - start);
|
||||
|
||||
@@ -292,6 +323,11 @@ private:
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// Check for function call syntax: identifier '(' args ')'
|
||||
skipSpaces();
|
||||
if (peek() == '(')
|
||||
return parseFunctionCall(token, result);
|
||||
|
||||
// It's an identifier — resolve via callback
|
||||
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
|
||||
result = 0;
|
||||
@@ -305,6 +341,71 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Built-in function call: vtop(pid, va), cr3(pid), phys(addr)
|
||||
bool parseFunctionCall(const QString& name, uint64_t& result) {
|
||||
advance(); // skip '('
|
||||
|
||||
if (name == QStringLiteral("vtop")) {
|
||||
// vtop(pid, virtualAddress) → physical address
|
||||
uint64_t pid = 0;
|
||||
if (!parseBitwiseOr(pid)) return false;
|
||||
skipSpaces();
|
||||
if (peek() != ',')
|
||||
return fail("vtop() requires 2 arguments: vtop(pid, va)");
|
||||
advance(); // skip ','
|
||||
uint64_t va = 0;
|
||||
if (!parseBitwiseOr(va)) return false;
|
||||
if (!expect(')')) return false;
|
||||
|
||||
if (!m_callbacks || !m_callbacks->vtop) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
result = m_callbacks->vtop((uint32_t)pid, va, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("vtop(0x%1, 0x%2) failed")
|
||||
.arg(pid, 0, 16).arg(va, 0, 16));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name == QStringLiteral("cr3")) {
|
||||
// cr3(pid) → CR3 value
|
||||
uint64_t pid = 0;
|
||||
if (!parseBitwiseOr(pid)) return false;
|
||||
if (!expect(')')) return false;
|
||||
|
||||
if (!m_callbacks || !m_callbacks->cr3) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
result = m_callbacks->cr3((uint32_t)pid, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("cr3(%1) failed").arg(pid));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name == QStringLiteral("phys")) {
|
||||
// phys(addr) → read 8 bytes from physical address
|
||||
uint64_t addr = 0;
|
||||
if (!parseBitwiseOr(addr)) return false;
|
||||
if (!expect(')')) return false;
|
||||
|
||||
if (!m_callbacks || !m_callbacks->physRead) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
result = m_callbacks->physRead(addr, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("phys(0x%1) failed").arg(addr, 0, 16));
|
||||
return true;
|
||||
}
|
||||
|
||||
return fail(QStringLiteral("unknown function '%1'").arg(name));
|
||||
}
|
||||
|
||||
// '[' bitwiseOr ']' — read the pointer value at the computed address
|
||||
bool parseDereference(uint64_t& result) {
|
||||
advance(); // skip '['
|
||||
|
||||
@@ -16,6 +16,11 @@ struct AddressParserCallbacks {
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
||||
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
|
||||
|
||||
// Kernel paging functions (optional — only wired when kernel provider active)
|
||||
std::function<uint64_t(uint32_t pid, uint64_t va, bool* ok)> vtop;
|
||||
std::function<uint64_t(uint32_t pid, bool* ok)> cr3;
|
||||
std::function<uint64_t(uint64_t physAddr, bool* ok)> physRead;
|
||||
};
|
||||
|
||||
class AddressParser {
|
||||
|
||||
368
src/compose.cpp
@@ -1,4 +1,5 @@
|
||||
#include "core.h"
|
||||
#include "typeinfer.h"
|
||||
#include "addressparser.h"
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
@@ -7,6 +8,49 @@ namespace rcx {
|
||||
|
||||
namespace {
|
||||
|
||||
// ── Value preview for type hints ──
|
||||
// Formats raw bytes as the suggested type using existing fmt:: functions.
|
||||
|
||||
static QString formatPreview(const uint8_t* data, int len, const TypeSuggestion& s) {
|
||||
using namespace detail;
|
||||
if (s.kinds.isEmpty()) return {};
|
||||
NodeKind k = s.kinds[0];
|
||||
if (s.kinds.size() == 1) {
|
||||
switch (k) {
|
||||
case NodeKind::Float: return fmt::fmtFloat(loadF32(data));
|
||||
case NodeKind::Double: return fmt::fmtDouble(loadF64(data));
|
||||
case NodeKind::Int32: return fmt::fmtInt32((int32_t)loadU32(data));
|
||||
case NodeKind::UInt32: return fmt::fmtUInt32(loadU32(data));
|
||||
case NodeKind::Int16: return fmt::fmtInt16((int16_t)loadU16(data));
|
||||
case NodeKind::UInt16: return fmt::fmtUInt16(loadU16(data));
|
||||
case NodeKind::Int64: return fmt::fmtInt64((int64_t)loadU64(data));
|
||||
case NodeKind::UInt64: return fmt::fmtUInt64(loadU64(data));
|
||||
case NodeKind::Pointer64: return fmt::fmtPointer64(loadU64(data));
|
||||
case NodeKind::Pointer32: return fmt::fmtPointer32(loadU32(data));
|
||||
case NodeKind::Bool: return fmt::fmtBool(data[0]);
|
||||
case NodeKind::UTF8: {
|
||||
int n = std::min(len, 8);
|
||||
QString s;
|
||||
for (int i = 0; i < n && data[i] >= 0x20 && data[i] <= 0x7E; ++i)
|
||||
s += QLatin1Char(data[i]);
|
||||
return s.isEmpty() ? QString() : (QStringLiteral("\"") + s + QStringLiteral("\""));
|
||||
}
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
// Split: show each part
|
||||
int partSz = len / s.kinds.size();
|
||||
QStringList parts;
|
||||
for (int i = 0; i < s.kinds.size(); ++i) {
|
||||
TypeSuggestion sub;
|
||||
sub.kinds = {s.kinds[i]};
|
||||
sub.score = s.score;
|
||||
sub.strength = s.strength;
|
||||
parts << formatPreview(data + i * partSz, partSz, sub);
|
||||
}
|
||||
return parts.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
// Scintilla fold constants (avoid including Scintilla headers in core)
|
||||
constexpr int SC_FOLDLEVELBASE = 0x400;
|
||||
constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000;
|
||||
@@ -24,6 +68,11 @@ struct ComposeState {
|
||||
int offsetHexDigits = 8; // hex digit tier for offset margin
|
||||
bool baseEmitted = false; // only first root struct shows base address
|
||||
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
||||
bool treeLines = false; // draw Unicode tree connectors in indentation
|
||||
bool braceWrap = false; // opening brace on its own line
|
||||
bool typeHints = false; // show type inference hints on hex nodes
|
||||
SymbolLookupFn symbolLookup; // optional PDB symbol lookup callback
|
||||
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
|
||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||
|
||||
// Precomputed for O(1) lookups
|
||||
@@ -41,7 +90,16 @@ struct ComposeState {
|
||||
return scopeNameW.value(scopeId, nameW);
|
||||
}
|
||||
|
||||
void emitLine(const QString& lineText, LineMeta lm) {
|
||||
// Set sibling-continuation flag for children at the given depth.
|
||||
// childDepth is the depth of the children being iterated.
|
||||
void setTreeSibling(int childDepth, bool hasMoreSiblings) {
|
||||
if (!treeLines) return;
|
||||
int d = childDepth - 1;
|
||||
while (siblingStack.size() <= d) siblingStack.append(false);
|
||||
siblingStack[d] = hasMoreSiblings;
|
||||
}
|
||||
|
||||
void emitLine(const QString& lineText, LineMeta&& lm) {
|
||||
if (currentLine > 0) text += '\n';
|
||||
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
|
||||
// CommandRow has no fold prefix (flush left)
|
||||
@@ -52,8 +110,30 @@ struct ComposeState {
|
||||
text += lm.foldCollapsed ? QStringLiteral(" \u25B8 ") : QStringLiteral(" \u25BE ");
|
||||
else
|
||||
text += QStringLiteral(" ");
|
||||
text += lineText;
|
||||
meta.append(lm);
|
||||
|
||||
// Replace leading indent spaces with Unicode tree connectors
|
||||
if (treeLines && lm.depth > 0) {
|
||||
QString treeIndent;
|
||||
int D = lm.depth;
|
||||
bool isFooter = (lm.lineKind == LineKind::Footer);
|
||||
for (int d = 0; d < D; d++) {
|
||||
bool active = (d < siblingStack.size() && siblingStack[d]);
|
||||
if (isFooter || d < D - 1) {
|
||||
// Ancestor continuation or footer's own level
|
||||
treeIndent += active ? QStringLiteral("\u2502 ")
|
||||
: QStringLiteral(" ");
|
||||
} else {
|
||||
// This node's own connector (non-footer only)
|
||||
treeIndent += active ? QStringLiteral("\u251C\u2500 ")
|
||||
: QStringLiteral("\u2514\u2500 ");
|
||||
}
|
||||
}
|
||||
text += treeIndent + lineText.mid(D * 3);
|
||||
} else {
|
||||
text += lineText;
|
||||
}
|
||||
|
||||
meta.append(std::move(lm));
|
||||
currentLine++;
|
||||
}
|
||||
};
|
||||
@@ -174,7 +254,37 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||
state.compactColumns);
|
||||
state.emitLine(lineText, lm);
|
||||
|
||||
// Type inference hint for hex nodes (when enabled)
|
||||
if (state.typeHints && isHexNode(node.kind) && sub == 0) {
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(absAddr, sz)
|
||||
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
|
||||
auto suggestions = inferTypes(
|
||||
reinterpret_cast<const uint8_t*>(b.constData()), sz);
|
||||
if (!suggestions.isEmpty() && suggestions[0].strength >= 3) {
|
||||
lm.typeHintStart = kFoldCol + lineText.size() + 2; // after fold prefix + " " gap
|
||||
lm.typeHintKinds = suggestions[0].kinds;
|
||||
QString typeName = formatHint(suggestions[0]);
|
||||
QString preview = formatPreview(
|
||||
reinterpret_cast<const uint8_t*>(b.constData()), sz, suggestions[0]);
|
||||
// Value-first with bracketed type: "0x7ff718570000 [ptr64]"
|
||||
if (!preview.isEmpty())
|
||||
lm.typeHint = preview + QStringLiteral(" [") + typeName + QStringLiteral("]");
|
||||
else
|
||||
lm.typeHint = QStringLiteral("[") + typeName + QStringLiteral("]");
|
||||
lineText += QStringLiteral(" ") + lm.typeHint;
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +322,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") +
|
||||
node.name + QStringLiteral(" */"), lm);
|
||||
node.name + QStringLiteral(" */"), std::move(lm));
|
||||
return;
|
||||
}
|
||||
state.visiting.insert(node.id);
|
||||
@@ -233,7 +343,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.arrayElementIdx = arrayElementIdx;
|
||||
uint64_t relOff = absAddr - arrayContainerAddr;
|
||||
QString relOffHex = QString::number(relOff, 16).toUpper();
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), lm);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), std::move(lm));
|
||||
}
|
||||
|
||||
// Detect root header: first root-level struct — suppressed from display
|
||||
@@ -286,7 +396,24 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.effectiveNameW = nameW;
|
||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
||||
}
|
||||
state.emitLine(headerText, lm);
|
||||
// Brace wrapping: move trailing '{' to its own line
|
||||
if (state.braceWrap && !node.collapsed && headerText.endsWith(QChar('{'))) {
|
||||
headerText.chop(1);
|
||||
// Remove trailing separator spaces
|
||||
while (headerText.endsWith(' ')) headerText.chop(1);
|
||||
state.emitLine(headerText, std::move(lm));
|
||||
// Emit standalone brace line
|
||||
LineMeta braceLm;
|
||||
braceLm.nodeIdx = nodeIdx;
|
||||
braceLm.nodeId = node.id;
|
||||
braceLm.depth = depth;
|
||||
braceLm.lineKind = LineKind::Header;
|
||||
braceLm.foldLevel = computeFoldLevel(depth, true);
|
||||
braceLm.markerMask = (1u << M_STRUCT_BG);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
|
||||
} else {
|
||||
state.emitLine(headerText, std::move(lm));
|
||||
}
|
||||
}
|
||||
|
||||
if (!node.collapsed || isArrayChild || isRootHeader) {
|
||||
@@ -305,6 +432,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
});
|
||||
|
||||
for (int oi = 0; oi < order.size(); oi++) {
|
||||
state.setTreeSibling(childDepth, oi < order.size() - 1);
|
||||
int mi = order[oi];
|
||||
const auto& m = node.enumMembers[mi];
|
||||
LineMeta lm;
|
||||
@@ -320,7 +448,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), lm);
|
||||
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), std::move(lm));
|
||||
}
|
||||
|
||||
// Footer
|
||||
@@ -337,7 +465,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, 0), std::move(lm));
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
@@ -353,6 +481,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
maxNameLen = qMax(maxNameLen, (int)m.name.size());
|
||||
|
||||
for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) {
|
||||
state.setTreeSibling(childDepth, mi < node.bitfieldMembers.size() - 1);
|
||||
const auto& m = node.bitfieldMembers[mi];
|
||||
uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind,
|
||||
m.bitOffset, m.bitWidth);
|
||||
@@ -370,7 +499,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
|
||||
childDepth, maxNameLen), lm);
|
||||
childDepth, maxNameLen), std::move(lm));
|
||||
}
|
||||
|
||||
// Footer
|
||||
@@ -388,7 +517,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr + sz;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
@@ -415,7 +544,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
int eTW = state.effectiveTypeW(node.id);
|
||||
int eNW = state.effectiveNameW(node.id);
|
||||
for (int i = 0; i < node.arrayLen; i++) {
|
||||
uint64_t elemAddr = absAddr + i * elemSize;
|
||||
state.setTreeSibling(childDepth, i < node.arrayLen - 1);
|
||||
uint64_t elemAddr = absAddr + (uint64_t)i * elemSize;
|
||||
|
||||
// Type override: "float[0]", "uint32_t[1]", etc.
|
||||
QString elemTypeStr = fmt::typeNameRaw(node.elementKind)
|
||||
@@ -424,7 +554,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
Node elem;
|
||||
elem.kind = node.elementKind;
|
||||
elem.name = QString(); // no name for array elements
|
||||
elem.offset = node.offset + i * elemSize;
|
||||
elem.offset = node.offset + (int)((uint64_t)i * elemSize);
|
||||
elem.parentId = node.id;
|
||||
elem.id = 0;
|
||||
|
||||
@@ -447,7 +577,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
|
||||
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
|
||||
{}, eTW, eNW, elemTypeStr,
|
||||
state.compactColumns), lm);
|
||||
state.compactColumns), std::move(lm));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +590,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
int elemSize = tree.structSpan(node.refId, &state.childMap);
|
||||
if (elemSize <= 0) elemSize = 1;
|
||||
for (int i = 0; i < node.arrayLen; i++) {
|
||||
state.setTreeSibling(childDepth, i < node.arrayLen - 1);
|
||||
uint64_t elemBase = absAddr + (uint64_t)i * elemSize;
|
||||
// Use base offset that maps refStruct's children to the right provider address
|
||||
composeParent(state, tree, prov, refIdx, childDepth, elemBase, node.refId,
|
||||
@@ -476,7 +607,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
const QVector<int>& refChildren = childIndices(state, node.refId);
|
||||
// Use the referenced struct's scope widths (children come from there)
|
||||
uint64_t refScopeId = node.refId;
|
||||
for (int childIdx : refChildren) {
|
||||
for (int rci = 0; rci < refChildren.size(); rci++) {
|
||||
int childIdx = refChildren[rci];
|
||||
state.setTreeSibling(childDepth, rci < refChildren.size() - 1);
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
// Self-referential child → show as collapsed struct (non-expandable)
|
||||
if (state.visiting.contains(child.id)) {
|
||||
@@ -502,7 +635,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
||||
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
|
||||
/*collapsed=*/true, typeW, nameW, state.compactColumns), std::move(lm));
|
||||
continue;
|
||||
}
|
||||
composeNode(state, tree, prov, childIdx, childDepth,
|
||||
@@ -514,7 +647,13 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
// For arrays, render children as condensed (no header/footer for struct elements)
|
||||
bool childrenAreArrayElements = (node.kind == NodeKind::Array);
|
||||
int elementIdx = 0;
|
||||
for (int childIdx : regular) {
|
||||
for (int ri = 0; ri < regular.size(); ri++) {
|
||||
int childIdx = regular[ri];
|
||||
// A regular child has more siblings if there are more regular children
|
||||
// or if static fields follow after all regular children
|
||||
bool hasMore = (ri < regular.size() - 1)
|
||||
|| (!staticIdxs.isEmpty() && !node.collapsed);
|
||||
state.setTreeSibling(childDepth, hasMore);
|
||||
// Pass this container's id as the scope for children (for per-scope widths)
|
||||
// For array elements, also pass the element index for [N] separator
|
||||
composeNode(state, tree, prov, childIdx, childDepth, base, rootId,
|
||||
@@ -524,7 +663,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
|
||||
// ── Static fields: render after regular children, before footer ──
|
||||
if (!staticIdxs.isEmpty() && !node.collapsed) {
|
||||
if (!staticIdxs.isEmpty() && (!node.collapsed || isRootHeader)) {
|
||||
// Build identifier resolver for static field expressions
|
||||
auto makeResolver = [&](uint64_t parentAbsAddr) {
|
||||
AddressParserCallbacks cbs;
|
||||
@@ -564,12 +703,19 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
*ok = false;
|
||||
return 0;
|
||||
};
|
||||
cbs.resolveModule = [&prov](const QString& name, bool* ok) -> uint64_t {
|
||||
uint64_t base = prov.symbolToAddress(name);
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
return cbs;
|
||||
};
|
||||
|
||||
auto cbs = makeResolver(absAddr);
|
||||
|
||||
for (int si : staticIdxs) {
|
||||
for (int sii = 0; sii < staticIdxs.size(); sii++) {
|
||||
int si = staticIdxs[sii];
|
||||
state.setTreeSibling(childDepth, sii < staticIdxs.size() - 1);
|
||||
const Node& sf = tree.nodes[si];
|
||||
|
||||
// Evaluate expression → absolute address
|
||||
@@ -635,12 +781,22 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
|
||||
lm.effectiveNameW = sf.name.size();
|
||||
state.emitLine(headerLine, lm);
|
||||
state.emitLine(headerLine, std::move(lm));
|
||||
|
||||
// ── Body + children (only when expanded) ──
|
||||
if (!isCollapsed) {
|
||||
// Determine if struct children follow the body line
|
||||
bool hasStructKids = exprOk
|
||||
&& (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array);
|
||||
const QVector<int> staticKids = hasStructKids
|
||||
? childIndices(state, sf.id) : QVector<int>();
|
||||
hasStructKids = hasStructKids && !staticKids.isEmpty();
|
||||
|
||||
// Body line: " return <expr> → 0xADDR"
|
||||
{
|
||||
// Body has more siblings if struct children follow
|
||||
state.setTreeSibling(childDepth + 1, hasStructKids);
|
||||
|
||||
QString bodyLine;
|
||||
if (!sf.offsetExpr.isEmpty()) {
|
||||
if (exprOk)
|
||||
@@ -672,18 +828,55 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
blm.offsetText = QString(state.offsetHexDigits, QChar(' '));
|
||||
blm.offsetAddr = staticAddr;
|
||||
blm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(bodyLine, blm);
|
||||
state.emitLine(bodyLine, std::move(blm));
|
||||
}
|
||||
|
||||
// If struct/array, compose children at evaluated address
|
||||
if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
|
||||
const QVector<int>& staticKids = childIndices(state, sf.id);
|
||||
for (int sci : staticKids) {
|
||||
composeNode(state, tree, prov, sci, childDepth + 1,
|
||||
if (hasStructKids) {
|
||||
for (int ski = 0; ski < staticKids.size(); ski++) {
|
||||
state.setTreeSibling(childDepth + 1, ski < staticKids.size() - 1);
|
||||
composeNode(state, tree, prov, staticKids[ski], childDepth + 1,
|
||||
staticAddr, sf.id, false, sf.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Static pointer: read pointer value at evaluated addr, expand ref struct
|
||||
if (exprOk && sf.refId != 0
|
||||
&& (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)) {
|
||||
int psz = sf.byteSize();
|
||||
uint64_t ptrVal = 0;
|
||||
if (prov.isValid() && psz > 0 && prov.isReadable(staticAddr, psz)) {
|
||||
ptrVal = (sf.kind == NodeKind::Pointer32)
|
||||
? (uint64_t)prov.readU32(staticAddr) : prov.readU64(staticAddr);
|
||||
if (ptrVal == UINT64_MAX || (sf.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
|
||||
ptrVal = 0;
|
||||
}
|
||||
// Relative pointer (RVA): target = base + value
|
||||
if (sf.isRelative && ptrVal != 0)
|
||||
ptrVal += absAddr;
|
||||
|
||||
if (ptrVal != 0) {
|
||||
uint64_t pBase = ptrVal;
|
||||
bool ptrReadable = prov.isReadable(pBase, 1);
|
||||
static NullProvider s_nullProv2;
|
||||
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv2);
|
||||
if (!ptrReadable) pBase = 0;
|
||||
|
||||
int refIdx = tree.indexOfId(sf.refId);
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
|
||||
uint64_t savedPtrBase = state.currentPtrBase;
|
||||
state.currentPtrBase = pBase;
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
childDepth, pBase, ref.id,
|
||||
/*isArrayChild=*/true);
|
||||
state.currentPtrBase = savedPtrBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer line: "};"
|
||||
{
|
||||
LineMeta flm;
|
||||
@@ -705,7 +898,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
flm.offsetAddr = staticAddr;
|
||||
}
|
||||
flm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm);
|
||||
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), std::move(flm));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,7 +920,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr + sz;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
@@ -750,6 +943,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
&& node.refId != 0) {
|
||||
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
if (node.isRelative)
|
||||
ptrTypeOverride += QStringLiteral(" rva");
|
||||
|
||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||
const QVector<int>& ptrChildren = childIndices(state, node.id);
|
||||
@@ -783,9 +978,26 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||
prov, absAddr, ptrTypeOverride,
|
||||
typeW, nameW, state.compactColumns), lm);
|
||||
{
|
||||
QString ptrText = fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||
prov, absAddr, ptrTypeOverride,
|
||||
typeW, nameW, state.compactColumns);
|
||||
if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) {
|
||||
ptrText.chop(1);
|
||||
while (ptrText.endsWith(' ')) ptrText.chop(1);
|
||||
state.emitLine(ptrText, std::move(lm));
|
||||
LineMeta braceLm;
|
||||
braceLm.nodeIdx = nodeIdx;
|
||||
braceLm.nodeId = node.id;
|
||||
braceLm.depth = depth;
|
||||
braceLm.lineKind = LineKind::Header;
|
||||
braceLm.foldLevel = computeFoldLevel(depth, true);
|
||||
braceLm.markerMask = lm.markerMask;
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
|
||||
} else {
|
||||
state.emitLine(ptrText, std::move(lm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!effectiveCollapsed) {
|
||||
@@ -801,7 +1013,10 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
}
|
||||
|
||||
// Pointer target address is used directly (absolute)
|
||||
// Relative pointer (RVA): target = base + value
|
||||
if (node.isRelative && ptrVal != 0)
|
||||
ptrVal += base;
|
||||
|
||||
uint64_t pBase = ptrVal;
|
||||
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
|
||||
|
||||
@@ -818,8 +1033,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
// Render materialized children at the pointer target address.
|
||||
// These are real tree nodes with independent state — use rootId
|
||||
// so resolveAddr computes offsets relative to the pointer target.
|
||||
for (int childIdx : ptrChildren) {
|
||||
composeNode(state, tree, childProv, childIdx, depth + 1,
|
||||
for (int pci = 0; pci < ptrChildren.size(); pci++) {
|
||||
state.setTreeSibling(depth + 1, pci < ptrChildren.size() - 1);
|
||||
composeNode(state, tree, childProv, ptrChildren[pci], depth + 1,
|
||||
pBase, node.id, false, node.id);
|
||||
}
|
||||
} else {
|
||||
@@ -862,7 +1078,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText.clear();
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), lm);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), std::move(lm));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -878,9 +1094,14 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
} // anonymous namespace
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||
bool compactColumns) {
|
||||
bool compactColumns, bool treeLines, bool braceWrap,
|
||||
bool typeHints, SymbolLookupFn symbolLookup) {
|
||||
ComposeState state;
|
||||
state.compactColumns = compactColumns;
|
||||
state.treeLines = treeLines;
|
||||
state.braceWrap = braceWrap;
|
||||
state.typeHints = typeHints;
|
||||
state.symbolLookup = std::move(symbolLookup);
|
||||
|
||||
// Precompute parent→children map
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
@@ -893,10 +1114,32 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
});
|
||||
}
|
||||
|
||||
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
||||
// Pre-allocate output buffers (estimate ~3 lines per node, ~80 chars per line)
|
||||
state.meta.reserve(tree.nodes.size() * 3);
|
||||
state.text.reserve(tree.nodes.size() * 80);
|
||||
|
||||
// Precompute absolute offsets via BFS (O(N) — avoids per-node parent-chain walk)
|
||||
state.absOffsets.resize(tree.nodes.size());
|
||||
state.absOffsets.fill(0);
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
|
||||
if (tree.nodes[i].parentId == 0)
|
||||
state.absOffsets[i] = tree.nodes[i].offset;
|
||||
{
|
||||
QVector<int> bfsQueue;
|
||||
for (int i : state.childMap.value(0))
|
||||
bfsQueue.append(i);
|
||||
int front = 0;
|
||||
while (front < bfsQueue.size()) {
|
||||
int idx = bfsQueue[front++];
|
||||
int pi = tree.indexOfId(tree.nodes[idx].parentId);
|
||||
state.absOffsets[idx] = (pi >= 0 ? state.absOffsets[pi] : 0)
|
||||
+ tree.nodes[idx].offset;
|
||||
for (int ci : state.childMap.value(tree.nodes[idx].id))
|
||||
bfsQueue.append(ci);
|
||||
}
|
||||
}
|
||||
for (auto& v : state.absOffsets)
|
||||
v += tree.baseAddress;
|
||||
|
||||
// Compute hex digit tier from max absolute address
|
||||
{
|
||||
@@ -925,23 +1168,21 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
return fmt::typeNameRaw(n.kind);
|
||||
};
|
||||
|
||||
// Compute effective type column width from longest type name
|
||||
// Include struct/array headers which use "struct TypeName" or "type[count]" format
|
||||
// Pre-compute type name lengths (avoids re-creating temp QStrings in width loops)
|
||||
QVector<int> typeNameLens(tree.nodes.size());
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
typeNameLens[i] = nodeTypeName(tree.nodes[i]).size();
|
||||
|
||||
// Compute effective column widths from longest type/name in a single pass
|
||||
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
|
||||
int maxTypeLen = kMinTypeW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
|
||||
int maxNameLen = kMinNameW;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
maxTypeLen = qMax(maxTypeLen, typeNameLens[i]);
|
||||
if (!isHexPreview(tree.nodes[i].kind))
|
||||
maxNameLen = qMax(maxNameLen, (int)tree.nodes[i].name.size());
|
||||
}
|
||||
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
|
||||
|
||||
// Compute effective name column width from longest name
|
||||
// Include struct/array names - they now use columnar layout too
|
||||
int maxNameLen = kMinNameW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
// Skip hex (they show ASCII preview, not name column)
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||
}
|
||||
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
||||
|
||||
// Pre-compute per-scope widths (each container gets widths based on direct children only)
|
||||
@@ -955,7 +1196,10 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
|
||||
for (int childIdx : state.childMap.value(container.id)) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||
// Skip struct children — pointer headers shouldn't inflate sibling widths
|
||||
if (child.kind == NodeKind::Struct)
|
||||
continue;
|
||||
scopeMaxType = qMax(scopeMaxType, typeNameLens[childIdx]);
|
||||
|
||||
// Name width (skip hex, but include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
@@ -981,13 +1225,15 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
}
|
||||
|
||||
// Compute scope widths for root level (parentId == 0)
|
||||
// Include struct/array headers - they now use columnar layout too
|
||||
{
|
||||
int rootMaxType = kMinTypeW;
|
||||
int rootMaxName = kMinNameW;
|
||||
for (int childIdx : state.childMap.value(0)) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||
// Skip struct children — pointer headers shouldn't inflate sibling widths
|
||||
if (child.kind == NodeKind::Struct)
|
||||
continue;
|
||||
rootMaxType = qMax(rootMaxType, typeNameLens[childIdx]);
|
||||
|
||||
// Name width (skip hex, include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
@@ -1014,7 +1260,19 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
lm.markerMask = 0;
|
||||
lm.effectiveTypeW = state.typeW;
|
||||
lm.effectiveNameW = state.nameW;
|
||||
state.emitLine(cmdRowText, lm);
|
||||
state.emitLine(cmdRowText, std::move(lm));
|
||||
}
|
||||
|
||||
// Brace wrapping: emit standalone "{" after CommandRow
|
||||
if (state.braceWrap) {
|
||||
LineMeta braceLm;
|
||||
braceLm.nodeIdx = -1;
|
||||
braceLm.nodeId = 0; // not associated with any node (no hover)
|
||||
braceLm.depth = 0;
|
||||
braceLm.lineKind = LineKind::Header;
|
||||
braceLm.foldLevel = SC_FOLDLEVELBASE;
|
||||
braceLm.markerMask = 0;
|
||||
state.emitLine(QStringLiteral("{"), std::move(braceLm));
|
||||
}
|
||||
|
||||
const QVector<int>& roots = childIndices(state, 0);
|
||||
@@ -1026,7 +1284,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
composeNode(state, tree, prov, idx, 0);
|
||||
}
|
||||
|
||||
return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress} };
|
||||
return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress, treeLines} };
|
||||
}
|
||||
|
||||
QSet<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {
|
||||
|
||||
1153
src/controller.cpp
@@ -40,7 +40,10 @@ public:
|
||||
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||
}
|
||||
|
||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const;
|
||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
||||
bool treeLines = false, bool braceWrap = false,
|
||||
bool typeHints = false,
|
||||
SymbolLookupFn symbolLookup = {}) const;
|
||||
bool save(const QString& path);
|
||||
bool load(const QString& path);
|
||||
void loadData(const QString& binaryPath);
|
||||
@@ -90,6 +93,7 @@ public:
|
||||
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
||||
void renameNode(int nodeIdx, const QString& newName);
|
||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
|
||||
void removeNode(int nodeIdx);
|
||||
void toggleCollapse(int nodeIdx);
|
||||
void materializeRefChildren(int nodeIdx);
|
||||
@@ -127,6 +131,10 @@ public:
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
void setCompactColumns(bool v);
|
||||
void setTreeLines(bool v);
|
||||
void setBraceWrap(bool v);
|
||||
void setTypeHints(bool v);
|
||||
bool typeHints() const { return m_typeHints; }
|
||||
void resetProvider();
|
||||
|
||||
// MCP bridge accessors
|
||||
@@ -147,12 +155,17 @@ public:
|
||||
// Cross-tab type visibility: point at the project's full document list
|
||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||
|
||||
// Test accessor
|
||||
// Test accessors
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
const ComposeResult& lastResult() const { return m_lastResult; }
|
||||
int dataExtent() const { return computeDataExtent(); }
|
||||
|
||||
signals:
|
||||
void nodeSelected(int nodeIdx);
|
||||
void selectionChanged(int count);
|
||||
void contextMenuAboutToShow(QMenu* menu, int line);
|
||||
void requestOpenProviderTab(const QString& pluginId, const QString& target,
|
||||
const QString& title);
|
||||
|
||||
private:
|
||||
RcxDocument* m_doc;
|
||||
@@ -162,6 +175,9 @@ private:
|
||||
int m_anchorLine = -1;
|
||||
bool m_suppressRefresh = false;
|
||||
bool m_compactColumns = false;
|
||||
bool m_treeLines = false;
|
||||
bool m_braceWrap = false;
|
||||
bool m_typeHints = false;
|
||||
uint64_t m_viewRootId = 0;
|
||||
|
||||
// ── Saved sources for quick-switch ──
|
||||
@@ -181,6 +197,7 @@ private:
|
||||
QSet<int64_t> m_changedOffsets;
|
||||
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||
bool m_trackValues = true;
|
||||
int m_valueTrackCooldown = 0; // suppress value recording for N refresh cycles after clear
|
||||
uint64_t m_refreshGen = 0;
|
||||
uint64_t m_readGen = 0;
|
||||
bool m_readInFlight = false;
|
||||
|
||||
95
src/core.h
@@ -11,6 +11,7 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
@@ -85,8 +86,8 @@ inline constexpr KindMeta kKindMeta[] = {
|
||||
{NodeKind::Vec3, "Vec3", "vec3", 12, 1, 4, KF_Vector},
|
||||
{NodeKind::Vec4, "Vec4", "vec4", 16, 1, 4, KF_Vector},
|
||||
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
|
||||
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
|
||||
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
|
||||
{NodeKind::UTF8, "UTF8", "str", 1, 1, 1, KF_String},
|
||||
{NodeKind::UTF16, "UTF16", "wstr", 2, 1, 2, KF_String},
|
||||
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
|
||||
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
|
||||
};
|
||||
@@ -152,14 +153,11 @@ inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
inline QStringList allTypeNamesForUI(bool /*stripBrackets*/ = false) {
|
||||
QStringList out;
|
||||
out.reserve(std::size(kKindMeta));
|
||||
for (const auto& m : kKindMeta) {
|
||||
QString t = QString::fromLatin1(m.typeName);
|
||||
if (stripBrackets) t.remove(QStringLiteral("[]"));
|
||||
out << t;
|
||||
}
|
||||
for (const auto& m : kKindMeta)
|
||||
out << QString::fromLatin1(m.typeName);
|
||||
out.sort(Qt::CaseInsensitive);
|
||||
out.removeDuplicates();
|
||||
return out;
|
||||
@@ -199,9 +197,10 @@ struct Node {
|
||||
int offset = 0;
|
||||
bool isStatic = false; // static field — excluded from struct layout
|
||||
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
|
||||
bool isRelative = false; // Pointer: target = base + value (RVA) instead of absolute
|
||||
int arrayLen = 1; // Array: element count
|
||||
int strLen = 64;
|
||||
bool collapsed = false;
|
||||
bool collapsed = true;
|
||||
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
|
||||
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
||||
@@ -244,6 +243,8 @@ struct Node {
|
||||
o["isStatic"] = true;
|
||||
if (!offsetExpr.isEmpty())
|
||||
o["offsetExpr"] = offsetExpr;
|
||||
if (isRelative)
|
||||
o["isRelative"] = true;
|
||||
o["arrayLen"] = arrayLen;
|
||||
o["strLen"] = strLen;
|
||||
o["collapsed"] = collapsed;
|
||||
@@ -285,9 +286,10 @@ struct Node {
|
||||
n.offset = o["offset"].toInt(0);
|
||||
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
|
||||
n.offsetExpr = o["offsetExpr"].toString();
|
||||
n.isRelative = o["isRelative"].toBool(false);
|
||||
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
|
||||
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
|
||||
n.collapsed = o["collapsed"].toBool(false);
|
||||
n.collapsed = true; // Always load collapsed; user expands as needed
|
||||
n.refId = o["refId"].toString("0").toULongLong();
|
||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||
@@ -295,8 +297,8 @@ struct Node {
|
||||
QJsonArray arr = o["enumMembers"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
QJsonObject em = v.toObject();
|
||||
n.enumMembers.append({em["name"].toString(),
|
||||
em["value"].toString("0").toLongLong()});
|
||||
n.enumMembers.emplaceBack(em["name"].toString(),
|
||||
em["value"].toString("0").toLongLong());
|
||||
}
|
||||
}
|
||||
if (o.contains("bitfieldMembers")) {
|
||||
@@ -305,7 +307,7 @@ struct Node {
|
||||
QJsonObject bm = v.toObject();
|
||||
BitfieldMember m;
|
||||
m.name = bm["name"].toString();
|
||||
m.bitOffset = (uint8_t)bm["bitOffset"].toInt(0);
|
||||
m.bitOffset = (uint8_t)qBound(0, bm["bitOffset"].toInt(0), 255);
|
||||
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
|
||||
n.bitfieldMembers.append(m);
|
||||
}
|
||||
@@ -335,6 +337,7 @@ struct NodeTree {
|
||||
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
||||
uint64_t m_nextId = 1;
|
||||
mutable QHash<uint64_t, int> m_idCache;
|
||||
mutable QHash<uint64_t, QVector<int>> m_childCache;
|
||||
|
||||
int addNode(const Node& n) {
|
||||
Node copy = n;
|
||||
@@ -344,13 +347,15 @@ struct NodeTree {
|
||||
nodes.append(copy);
|
||||
if (!m_idCache.isEmpty())
|
||||
m_idCache[copy.id] = idx;
|
||||
if (!m_childCache.isEmpty())
|
||||
m_childCache[copy.parentId].append(idx);
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Reserve a unique ID atomically (for use before pushing undo commands)
|
||||
uint64_t reserveId() { return m_nextId++; }
|
||||
|
||||
void invalidateIdCache() const { m_idCache.clear(); }
|
||||
void invalidateIdCache() const { m_idCache.clear(); m_childCache.clear(); }
|
||||
|
||||
int indexOfId(uint64_t id) const {
|
||||
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
|
||||
@@ -361,11 +366,11 @@ struct NodeTree {
|
||||
}
|
||||
|
||||
QVector<int> childrenOf(uint64_t parentId) const {
|
||||
QVector<int> result;
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i].parentId == parentId) result.append(i);
|
||||
if (m_childCache.isEmpty() && !nodes.isEmpty()) {
|
||||
for (int i = 0; i < nodes.size(); i++)
|
||||
m_childCache[nodes[i].parentId].append(i);
|
||||
}
|
||||
return result;
|
||||
return m_childCache.value(parentId);
|
||||
}
|
||||
|
||||
// Collect node + all descendants (iterative, cycle-safe)
|
||||
@@ -449,8 +454,8 @@ struct NodeTree {
|
||||
if (c.isStatic) continue; // static fields don't affect struct size
|
||||
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
|
||||
? structSpan(c.id, childMap, visited) : c.byteSize();
|
||||
int end = c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = end;
|
||||
int64_t end = (int64_t)c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = (int)qMin(end, (int64_t)INT_MAX);
|
||||
}
|
||||
|
||||
// Embedded struct reference: no own children but refId points to a struct definition
|
||||
@@ -485,6 +490,7 @@ struct NodeTree {
|
||||
t.pointerSize = o["pointerSize"].toInt(8);
|
||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||
QJsonArray arr = o["nodes"].toArray();
|
||||
t.nodes.reserve(arr.size());
|
||||
for (const auto& v : arr) {
|
||||
Node n = Node::fromJson(v.toObject());
|
||||
t.nodes.append(n);
|
||||
@@ -500,6 +506,7 @@ struct NodeTree {
|
||||
struct ValueHistory {
|
||||
static constexpr int kCapacity = 10;
|
||||
std::array<QString, kCapacity> values;
|
||||
std::array<qint64, kCapacity> timestamps{}; // msec since epoch
|
||||
int count = 0; // total unique values recorded
|
||||
int head = 0; // next write position in ring
|
||||
|
||||
@@ -509,10 +516,16 @@ struct ValueHistory {
|
||||
if (values[last] == v) return; // no change
|
||||
}
|
||||
values[head] = v;
|
||||
timestamps[head] = QDateTime::currentMSecsSinceEpoch();
|
||||
head = (head + 1) % kCapacity;
|
||||
if (count < INT_MAX) count++;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
count = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||
|
||||
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||
@@ -536,6 +549,16 @@ struct ValueHistory {
|
||||
for (int i = 0; i < n; i++)
|
||||
fn(values[(start + i) % kCapacity]);
|
||||
}
|
||||
|
||||
// Iterate with timestamps from newest to oldest
|
||||
template<typename Fn>
|
||||
void forEachWithTime(Fn&& fn) const {
|
||||
int n = uniqueCount();
|
||||
for (int i = 0; i < n; i++) {
|
||||
int idx = (head + kCapacity - 1 - i) % kCapacity;
|
||||
fn(values[idx], timestamps[idx]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── LineMeta ──
|
||||
@@ -551,12 +574,13 @@ static constexpr int kCommandRowLine = 0;
|
||||
static constexpr int kFirstDataLine = 1;
|
||||
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
|
||||
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
|
||||
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
|
||||
static constexpr uint64_t kArrayElemShift = 42; // bits 42-61 hold element index
|
||||
static constexpr uint64_t kArrayElemMask = 0x3FFFFC0000000000ULL; // 20 bits → max 1048575 elements
|
||||
|
||||
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
|
||||
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 42)
|
||||
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
||||
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
|
||||
Q_ASSERT(elemIdx >= 0);
|
||||
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0xFFFFF) << kArrayElemShift);
|
||||
}
|
||||
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
||||
@@ -564,11 +588,11 @@ inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||
|
||||
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
|
||||
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
|
||||
static constexpr uint64_t kMemberSubShift = 48;
|
||||
static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL;
|
||||
static constexpr uint64_t kMemberSubShift = 42;
|
||||
static constexpr uint64_t kMemberSubMask = 0x3FFFFC0000000000ULL;
|
||||
|
||||
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
|
||||
return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift);
|
||||
return nodeId | kMemberBit | ((uint64_t)(subLine & 0xFFFFF) << kMemberSubShift);
|
||||
}
|
||||
inline int memberSubFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
|
||||
@@ -605,6 +629,9 @@ struct LineMeta {
|
||||
bool isArrayElement = false; // true for synthesized primitive array element lines
|
||||
bool isMemberLine = false; // true for enum member / bitfield member lines
|
||||
bool isStaticLine = false; // true for static field node lines
|
||||
QString typeHint; // Type inference hint text (e.g. "Float×2") — only set for hex nodes when hints enabled
|
||||
int typeHintStart = -1; // Character offset where hint text starts in line text (-1 = none)
|
||||
QVector<NodeKind> typeHintKinds; // Suggested kinds from inference (empty = no hint)
|
||||
};
|
||||
|
||||
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
@@ -618,6 +645,7 @@ struct LayoutInfo {
|
||||
int nameW = 22; // Effective name column width (default = kColName)
|
||||
int offsetHexDigits = 8; // Hex digits for offset margin (4/8/12/16)
|
||||
uint64_t baseAddress = 0; // Base address for relative offset computation
|
||||
bool treeLines = false; // Whether tree line connectors are embedded in the text
|
||||
};
|
||||
|
||||
// ── ComposeResult ──
|
||||
@@ -653,6 +681,7 @@ namespace cmd {
|
||||
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
|
||||
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
|
||||
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
|
||||
struct ToggleRelative { uint64_t nodeId; bool oldVal, newVal; };
|
||||
}
|
||||
|
||||
using Command = std::variant<
|
||||
@@ -660,7 +689,7 @@ using Command = std::variant<
|
||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
|
||||
cmd::ChangeOffsetExpr, cmd::ToggleStatic
|
||||
cmd::ChangeOffsetExpr, cmd::ToggleStatic, cmd::ToggleRelative
|
||||
>;
|
||||
|
||||
// ── Column spans (for inline editing) ──
|
||||
@@ -683,7 +712,7 @@ inline constexpr int kColValue = 96;
|
||||
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
||||
inline constexpr int kSepWidth = 1;
|
||||
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
|
||||
inline constexpr int kMinTypeW = 7; // Minimum type column width (fits "uint8_t")
|
||||
inline constexpr int kMaxTypeW = 128; // Maximum type column width
|
||||
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
||||
inline constexpr int kMaxNameW = 128; // Maximum name column width
|
||||
@@ -1014,7 +1043,13 @@ namespace fmt {
|
||||
|
||||
// ── Compose function forward declaration ──
|
||||
|
||||
// Optional callback: given an absolute address, return a symbol name (e.g. "nt!PsActiveProcessHead")
|
||||
// or empty string if no symbol matches. Used for PDB symbol annotations on rows.
|
||||
using SymbolLookupFn = std::function<QString(uint64_t addr)>;
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||
bool compactColumns = false);
|
||||
bool compactColumns = false, bool treeLines = false,
|
||||
bool braceWrap = false, bool typeHints = false,
|
||||
SymbolLookupFn symbolLookup = {});
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
36
src/dock_tab_buttons.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QToolButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
|
||||
// Dock tab button widget (close button)
|
||||
// Placed on the right side of each dock tab via QTabBar::setTabButton.
|
||||
class DockTabButtons : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
QToolButton* closeBtn;
|
||||
|
||||
explicit DockTabButtons(QWidget* parent = nullptr) : QWidget(parent) {
|
||||
auto* hl = new QHBoxLayout(this);
|
||||
hl->setContentsMargins(0, 0, 0, 0);
|
||||
hl->setSpacing(0);
|
||||
|
||||
closeBtn = new QToolButton(this);
|
||||
closeBtn->setAutoRaise(true);
|
||||
closeBtn->setCursor(Qt::PointingHandCursor);
|
||||
closeBtn->setFixedSize(16, 16);
|
||||
closeBtn->setToolTip("Close tab");
|
||||
closeBtn->setIcon(QIcon(":/vsicons/close.svg"));
|
||||
closeBtn->setIconSize(QSize(12, 12));
|
||||
hl->addWidget(closeBtn);
|
||||
}
|
||||
|
||||
void applyTheme(const QColor& hover) {
|
||||
QString style = QStringLiteral(
|
||||
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
|
||||
"QToolButton:hover { background: %1; }").arg(hover.name());
|
||||
closeBtn->setStyleSheet(style);
|
||||
}
|
||||
};
|
||||
1492
src/editor.cpp
42
src/editor.h
@@ -1,21 +1,18 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include "providerregistry.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QLineEdit;
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct SavedSourceDisplay {
|
||||
QString text;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
class RcxEditor : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -28,10 +25,15 @@ public:
|
||||
void restoreViewState(const ViewState& vs);
|
||||
|
||||
QsciScintilla* scintilla() const { return m_sci; }
|
||||
QWidget* historyPopup() const { return m_historyPopup; }
|
||||
QWidget* disasmPopup() const { return m_disasmPopup; }
|
||||
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
|
||||
const LineMeta* metaForLine(int line) const;
|
||||
int currentNodeIndex() const;
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
void showFindBar();
|
||||
void dismissHistoryPopup();
|
||||
void dismissAllPopups();
|
||||
|
||||
// ── Column span computation ──
|
||||
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||
@@ -45,6 +47,7 @@ public:
|
||||
bool isEditing() const { return m_editState.active; }
|
||||
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
|
||||
void cancelInlineEdit();
|
||||
void setHexEditPending(bool v) { m_hexEditPending = v; }
|
||||
void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; }
|
||||
|
||||
void applySelectionOverlay(const QSet<uint64_t>& selIds);
|
||||
@@ -78,6 +81,11 @@ signals:
|
||||
void inlineEditCancelled();
|
||||
void typeSelectorRequested();
|
||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||
void relativeOffsetsChanged(bool relative);
|
||||
void appendBytesRequested(uint64_t structId, int byteCount);
|
||||
void trimHexRequested(uint64_t structId);
|
||||
void appendEnumMembersRequested(uint64_t enumId, int count);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
@@ -132,6 +140,7 @@ private:
|
||||
NodeKind editKind = NodeKind::Int32;
|
||||
int commentCol = -1; // fixed comment column (stored at edit start)
|
||||
bool lastValidationOk = true; // track state to avoid redundant updates
|
||||
bool hexOverwrite = false; // true for hex-byte / ASCII-preview fixed-length editing
|
||||
};
|
||||
InlineEditState m_editState;
|
||||
QStringList m_staticCompletions; // autocomplete words for StaticExpr editing
|
||||
@@ -148,12 +157,22 @@ private:
|
||||
// ── Value history ref (owned by controller) ──
|
||||
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
|
||||
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
|
||||
QWidget* m_structPreviewPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
|
||||
QWidget* m_arrowTooltip = nullptr; // RcxTooltip (arrow callout)
|
||||
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
// ── Find bar ──
|
||||
QWidget* m_findBarContainer = nullptr;
|
||||
QLineEdit* m_findBar = nullptr;
|
||||
long m_findPos = 0;
|
||||
void hideFindBar();
|
||||
|
||||
// ── Hex inline edit ──
|
||||
bool m_hexEditPending = false; // set by context menu before calling beginInlineEdit
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
@@ -166,13 +185,11 @@ private:
|
||||
void setupMarkers();
|
||||
void allocateMarginStyles();
|
||||
|
||||
void applyMarginText(const QVector<LineMeta>& meta);
|
||||
void applyLineAttributes(const QVector<LineMeta>& meta);
|
||||
void reformatMargins();
|
||||
void applyMarkers(const QVector<LineMeta>& meta);
|
||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
|
||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||
void applyCommandRowPills();
|
||||
|
||||
@@ -180,6 +197,7 @@ private:
|
||||
int editEndCol() const;
|
||||
bool handleNormalKey(QKeyEvent* ke);
|
||||
bool handleEditKey(QKeyEvent* ke);
|
||||
bool handleHexEditKey(QKeyEvent* ke);
|
||||
void showTypeAutocomplete();
|
||||
void showSourcePicker();
|
||||
void showTypeListFiltered(const QString& filter);
|
||||
|
||||
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"]
|
||||
}
|
||||
1
src/examples/WinSDK.rcx
Normal file
10755
src/examples/t6zm.rcx
Normal file
42817
src/examples/windows-x86_64.h
Normal file
@@ -73,7 +73,7 @@ QString pointerTypeName(NodeKind kind, const QString& targetName) {
|
||||
// ── Value formatting ──
|
||||
|
||||
static QString hexVal(uint64_t v) {
|
||||
return QStringLiteral("0x") + QString::number(v, 16);
|
||||
return QString::asprintf("0x%llx", (unsigned long long)v);
|
||||
}
|
||||
|
||||
static QString rawHex(uint64_t v, int digits) {
|
||||
@@ -163,8 +163,13 @@ QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
|
||||
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
return indent(depth) + QStringLiteral("};");
|
||||
QString fmtStructFooter(const Node& node, int depth, int /*totalSize*/) {
|
||||
QString footer = indent(depth) + QStringLiteral("};");
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||
footer += QStringLiteral(" +10");
|
||||
else
|
||||
footer += QStringLiteral(" +10h +100h +1000h Trim");
|
||||
return footer;
|
||||
}
|
||||
|
||||
// ── Array header ──
|
||||
@@ -228,15 +233,18 @@ static QString bytesToAscii(const QByteArray& b, int slot) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static const char kHexDigits[] = "0123456789ABCDEF";
|
||||
|
||||
static QString bytesToHex(const QByteArray& b, int slot) {
|
||||
QString out;
|
||||
out.reserve(slot * 3);
|
||||
QChar buf[64]; // max slot=8 → 8*3-1=23 chars; 64 is plenty
|
||||
int pos = 0;
|
||||
for (int i = 0; i < slot; ++i) {
|
||||
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
|
||||
out += QString::asprintf("%02X", (unsigned)c);
|
||||
if (i + 1 < slot) out += ' ';
|
||||
buf[pos++] = QLatin1Char(kHexDigits[c >> 4]);
|
||||
buf[pos++] = QLatin1Char(kHexDigits[c & 0xF]);
|
||||
if (i + 1 < slot) buf[pos++] = QLatin1Char(' ');
|
||||
}
|
||||
return out;
|
||||
return QString(buf, pos);
|
||||
}
|
||||
|
||||
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
|
||||
@@ -653,8 +661,10 @@ QString validateValue(NodeKind kind, const QString& text) {
|
||||
QString digits = hasHexPrefix ? s.mid(2) : s;
|
||||
|
||||
if (hasHexPrefix || isHexKind) {
|
||||
// Hex mode: only 0-9, a-f, A-F
|
||||
// Hex mode: only 0-9, a-f, A-F (spaces allowed for multi-byte hex kinds)
|
||||
bool isMultiByteHex = (kind >= NodeKind::Hex16 && kind <= NodeKind::Hex64);
|
||||
for (QChar c : digits) {
|
||||
if (c == ' ' && isMultiByteHex) continue;
|
||||
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
|
||||
return QStringLiteral("invalid hex '%1'").arg(c);
|
||||
}
|
||||
@@ -715,6 +725,7 @@ uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
case NodeKind::Hex32: container = prov.readU32(addr); break;
|
||||
default: container = prov.readU64(addr); break;
|
||||
}
|
||||
Q_ASSERT(bitOffset + bitWidth <= 64);
|
||||
if (bitWidth >= 64) return container >> bitOffset;
|
||||
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
|
||||
}
|
||||
|
||||
1176
src/generator.cpp
@@ -6,17 +6,83 @@
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Generate C++ struct definitions for a single root struct and all
|
||||
// nested/referenced types reachable from it.
|
||||
// ── Code output format ──
|
||||
|
||||
enum class CodeFormat : int {
|
||||
CppHeader = 0, // C/C++ struct definitions
|
||||
RustStruct, // Rust #[repr(C)] struct definitions
|
||||
DefineOffsets, // #define ClassName_FieldName 0xNN
|
||||
CSharpStruct, // C# [StructLayout] with [FieldOffset]
|
||||
PythonCtypes, // Python ctypes.Structure
|
||||
_Count
|
||||
};
|
||||
|
||||
enum class CodeScope : int {
|
||||
Current = 0, // Just the selected struct
|
||||
WithChildren, // Selected struct + all referenced types
|
||||
FullSdk, // All root-level structs
|
||||
_Count
|
||||
};
|
||||
|
||||
const char* codeFormatName(CodeFormat fmt);
|
||||
const char* codeFormatFileFilter(CodeFormat fmt);
|
||||
const char* codeScopeName(CodeScope scope);
|
||||
|
||||
// ── Format-aware dispatch (calls the appropriate backend) ──
|
||||
|
||||
QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// Render rootStructId + all struct types reachable from it
|
||||
QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderCodeAll(CodeFormat fmt, const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// ── Individual backends ──
|
||||
|
||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// Generate C++ struct definitions for every root-level struct (full SDK).
|
||||
QString renderCppTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCppAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderRust(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderRustTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderRustAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderDefines(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderDefinesTree(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderDefinesAll(const NodeTree& tree);
|
||||
|
||||
QString renderCSharp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCSharpAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderPython(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderPythonTree(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderPythonAll(const NodeTree& tree);
|
||||
|
||||
// Null generator placeholder (returns empty string).
|
||||
QString renderNull(const NodeTree& tree, uint64_t rootStructId);
|
||||
|
||||
|
||||
BIN
src/icons/class.icns
Normal file
@@ -396,7 +396,7 @@ uint64_t PdbCtx::importEnum(uint32_t typeIndex) {
|
||||
field->data.LF_ENUMERATE.value,
|
||||
field->data.LF_ENUMERATE.lfEasy.kind);
|
||||
if (eName)
|
||||
s.enumMembers.append({QString::fromUtf8(eName), val});
|
||||
s.enumMembers.emplaceBack(QString::fromUtf8(eName), val);
|
||||
|
||||
i += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
|
||||
i += strnlen(eName, maxSize - i - 1) + 1;
|
||||
@@ -880,7 +880,7 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
n.name = name;
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
n.bitfieldMembers.append({name, bitPos, bitLen});
|
||||
n.bitfieldMembers.push_back(BitfieldMember{name, bitPos, bitLen});
|
||||
tree.addNode(n);
|
||||
break;
|
||||
}
|
||||
@@ -943,6 +943,123 @@ 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;
|
||||
uint32_t typeIdx = 0u;
|
||||
|
||||
if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32) {
|
||||
name = record->data.S_GDATA32.name;
|
||||
typeIdx = record->data.S_GDATA32.typeIndex;
|
||||
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;
|
||||
typeIdx = record->data.S_GTHREAD32.typeIndex;
|
||||
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;
|
||||
typeIdx = record->data.S_LDATA32.typeIndex;
|
||||
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;
|
||||
typeIdx = record->data.S_LTHREAD32.typeIndex;
|
||||
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, typeIdx});
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "[PDB] extractPdbSymbols:" << result.symbols.size() << "symbols from"
|
||||
<< result.moduleName;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Public API: enumeratePdbTypes ──
|
||||
|
||||
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg) {
|
||||
@@ -1120,12 +1237,130 @@ NodeTree importPdb(const QString& pdbPath, const QString& structFilter, QString*
|
||||
return ctx.tree;
|
||||
}
|
||||
|
||||
// ── Public API: importTypeForSymbol ──
|
||||
|
||||
NodeTree importTypeForSymbol(const QString& pdbPath,
|
||||
uint32_t typeIndex,
|
||||
QString* typeName,
|
||||
QString* errorMsg) {
|
||||
auto setErr = [&](const QString& msg) { if (errorMsg) *errorMsg = msg; };
|
||||
|
||||
if (typeIndex == 0) {
|
||||
setErr(QStringLiteral("Symbol has no associated type (typeIndex=0)"));
|
||||
return {};
|
||||
}
|
||||
|
||||
PdbFile pdb;
|
||||
if (!pdb.open(pdbPath, errorMsg)) return {};
|
||||
|
||||
const TypeTable& tt = *pdb.typeTable;
|
||||
|
||||
// Walk through LF_MODIFIER and LF_POINTER chains to find the underlying UDT/enum
|
||||
uint32_t ti = typeIndex;
|
||||
int depth = 0;
|
||||
while (ti >= tt.firstIndex() && depth < 16) {
|
||||
const auto* rec = tt.get(ti);
|
||||
if (!rec) break;
|
||||
|
||||
if (rec->header.kind == TRK::LF_MODIFIER) {
|
||||
ti = rec->data.LF_MODIFIER.type;
|
||||
depth++;
|
||||
continue;
|
||||
}
|
||||
if (rec->header.kind == TRK::LF_POINTER) {
|
||||
ti = rec->data.LF_POINTER.utype;
|
||||
depth++;
|
||||
continue;
|
||||
}
|
||||
break; // reached a non-wrapper type
|
||||
}
|
||||
|
||||
// Check if we landed on a UDT or enum
|
||||
if (ti < tt.firstIndex()) {
|
||||
setErr(QStringLiteral("Symbol type resolves to a primitive (typeIndex %1)")
|
||||
.arg(typeIndex));
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto* rec = tt.get(ti);
|
||||
if (!rec) {
|
||||
setErr(QStringLiteral("Invalid type index %1").arg(ti));
|
||||
return {};
|
||||
}
|
||||
|
||||
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
|
||||
rec->header.kind == TRK::LF_CLASS ||
|
||||
rec->header.kind == TRK::LF_UNION);
|
||||
bool isEnum = (rec->header.kind == TRK::LF_ENUM);
|
||||
|
||||
if (!isUDT && !isEnum) {
|
||||
setErr(QStringLiteral("Symbol type is not a struct/class/union/enum (kind 0x%1)")
|
||||
.arg((uint16_t)rec->header.kind, 0, 16));
|
||||
return {};
|
||||
}
|
||||
|
||||
// Extract the type name for the caller
|
||||
if (typeName) {
|
||||
const char* name = nullptr;
|
||||
if (isEnum) {
|
||||
name = rec->data.LF_ENUM.name;
|
||||
} else if (rec->header.kind == TRK::LF_UNION) {
|
||||
name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
|
||||
} else {
|
||||
name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
||||
}
|
||||
if (name) *typeName = QString::fromUtf8(name);
|
||||
}
|
||||
|
||||
// If this is a forward reference, resolve to the full definition
|
||||
PdbCtx ctx;
|
||||
ctx.tt = &tt;
|
||||
|
||||
if (isUDT) {
|
||||
bool fwdref = false;
|
||||
if (rec->header.kind == TRK::LF_UNION)
|
||||
fwdref = rec->data.LF_UNION.property.fwdref;
|
||||
else
|
||||
fwdref = rec->data.LF_CLASS.property.fwdref;
|
||||
|
||||
if (fwdref) {
|
||||
// Build the definition index to find the real definition
|
||||
ctx.buildUdtDefinitionIndex();
|
||||
const char* name = nullptr;
|
||||
if (rec->header.kind == TRK::LF_UNION)
|
||||
name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
|
||||
else
|
||||
name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
||||
|
||||
if (name) {
|
||||
uint32_t defTi = ctx.findUdtDefinitionIndex(rec->header.kind, name);
|
||||
if (defTi != 0)
|
||||
ti = defTi;
|
||||
}
|
||||
}
|
||||
ctx.importUDT(ti);
|
||||
} else {
|
||||
ctx.importEnum(ti);
|
||||
}
|
||||
|
||||
if (ctx.tree.nodes.isEmpty()) {
|
||||
setErr(QStringLiteral("Failed to import type at index %1").arg(ti));
|
||||
}
|
||||
|
||||
return ctx.tree;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
#else // !_WIN32
|
||||
|
||||
namespace rcx {
|
||||
|
||||
PdbSymbolResult extractPdbSymbols(const QString&, QString* errorMsg) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
|
||||
return {};
|
||||
}
|
||||
|
||||
QVector<PdbTypeInfo> enumeratePdbTypes(const QString&, QString* errorMsg) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
|
||||
return {};
|
||||
@@ -1142,6 +1377,11 @@ NodeTree importPdb(const QString&, const QString&, QString* errorMsg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
NodeTree importTypeForSymbol(const QString&, uint32_t, QString*, QString* errorMsg) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,26 @@
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── PDB Symbol Extraction ──
|
||||
|
||||
struct PdbSymbol {
|
||||
QString name;
|
||||
uint32_t rva;
|
||||
uint32_t typeIndex = 0; // TPI type index (0 = unknown / public symbol)
|
||||
};
|
||||
|
||||
struct PdbSymbolResult {
|
||||
QString moduleName; // derived from PDB filename (e.g. "ntoskrnl")
|
||||
QVector<PdbSymbol> symbols;
|
||||
};
|
||||
|
||||
// Extract public/global symbols (name → RVA) from a PDB file.
|
||||
// This reads the DBI stream's public and global symbol sub-streams.
|
||||
PdbSymbolResult extractPdbSymbols(const QString& pdbPath,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// ── PDB Type Import ──
|
||||
|
||||
struct PdbTypeInfo {
|
||||
uint32_t typeIndex; // TPI type index
|
||||
QString name; // struct/class/union/enum name
|
||||
@@ -32,4 +52,12 @@ NodeTree importPdb(const QString& pdbPath,
|
||||
const QString& structFilter = {},
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Import the type associated with a global symbol's typeIndex.
|
||||
// Opens the PDB, resolves the typeIndex to a UDT/enum, and returns the imported tree.
|
||||
// Returns empty tree if the symbol has no associated type or the type is a simple primitive.
|
||||
NodeTree importTypeForSymbol(const QString& pdbPath,
|
||||
uint32_t typeIndex,
|
||||
QString* typeName = nullptr,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -294,7 +294,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointe
|
||||
|
||||
// Defer ref resolution if array references a class
|
||||
if (!arrayClassName.isEmpty()) {
|
||||
pendingRefs.append({arrId, arrayClassName});
|
||||
pendingRefs.push_back(PendingRef{arrId, arrayClassName});
|
||||
}
|
||||
|
||||
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||
@@ -321,7 +321,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointe
|
||||
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, ptrClass});
|
||||
pendingRefs.push_back(PendingRef{nodeId, ptrClass});
|
||||
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||
continue;
|
||||
}
|
||||
@@ -335,7 +335,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointe
|
||||
if (!n.structTypeName.isEmpty()) {
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, n.structTypeName});
|
||||
pendingRefs.push_back(PendingRef{nodeId, n.structTypeName});
|
||||
} else {
|
||||
tree.addNode(n);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
|
||||
t[QStringLiteral("USHORT")] = {NodeKind::UInt16, 2};
|
||||
t[QStringLiteral("SHORT")] = {NodeKind::Int16, 2};
|
||||
t[QStringLiteral("WCHAR")] = {NodeKind::UInt16, 2};
|
||||
t[QStringLiteral("TCHAR")] = {NodeKind::UInt16, 2};
|
||||
t[QStringLiteral("DWORD")] = {NodeKind::UInt32, 4};
|
||||
t[QStringLiteral("ULONG")] = {NodeKind::UInt32, 4};
|
||||
t[QStringLiteral("UINT")] = {NodeKind::UInt32, 4};
|
||||
@@ -199,10 +200,10 @@ struct Tokenizer {
|
||||
case '=': tk = TokKind::Equals; break;
|
||||
default: tk = TokKind::Other; break;
|
||||
}
|
||||
tokens.append({tk, QString(c), line});
|
||||
tokens.push_back(Token{tk, QString(c), line});
|
||||
pos++;
|
||||
}
|
||||
tokens.append({TokKind::Eof, {}, line});
|
||||
tokens.push_back(Token{TokKind::Eof, {}, line});
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -240,7 +241,7 @@ private:
|
||||
bool ok;
|
||||
int val = m.captured(1).toInt(&ok, 16);
|
||||
if (ok) {
|
||||
offsets.append({commentLine, val});
|
||||
offsets.push_back(LineOffset{commentLine, val});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,7 +259,7 @@ private:
|
||||
void parseIdent() {
|
||||
int start = pos;
|
||||
while (pos < src.size() && (src[pos].isLetterOrNumber() || src[pos] == '_')) pos++;
|
||||
tokens.append({TokKind::Ident, src.mid(start, pos - start), line});
|
||||
tokens.push_back(Token{TokKind::Ident, src.mid(start, pos - start), line});
|
||||
}
|
||||
|
||||
void parseNumber() {
|
||||
@@ -275,7 +276,7 @@ private:
|
||||
// Skip integer suffixes (U, L, LL, ULL, etc.)
|
||||
while (pos < src.size() && (src[pos] == 'u' || src[pos] == 'U' ||
|
||||
src[pos] == 'l' || src[pos] == 'L')) pos++;
|
||||
tokens.append({TokKind::Number, src.mid(start, pos - start), line});
|
||||
tokens.push_back(Token{TokKind::Number, src.mid(start, pos - start), line});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -332,7 +333,10 @@ struct Parser {
|
||||
QVector<ParsedStruct> structs;
|
||||
QSet<QString> forwardDecls;
|
||||
QHash<QString, QString> typedefs; // alias -> real type
|
||||
QSet<QString> pointerTypedefs; // aliases that are pointer-to-struct
|
||||
QHash<QString, QVector<int>> arrayTypedefs; // aliases that are array types (alias -> dimensions)
|
||||
QHash<QString, int> sizeAsserts; // struct name -> declared size
|
||||
QHash<QString, int> structAlignments; // struct name -> ALIGN(N) value
|
||||
|
||||
explicit Parser(const QVector<Token>& t, const QVector<LineOffset>& lo)
|
||||
: tokens(t), lineOffsets(lo) {}
|
||||
@@ -375,12 +379,57 @@ struct Parser {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip ALIGN( N ) macro if present (Vergilius-style headers)
|
||||
// Returns the alignment value, or 0 if no ALIGN macro.
|
||||
int skipAlignMacro() {
|
||||
if (checkIdent("ALIGN") || checkIdent("__declspec")) {
|
||||
advance();
|
||||
int alignVal = 0;
|
||||
if (match(TokKind::LParen)) {
|
||||
// Try to read the alignment number
|
||||
if (peek().kind == TokKind::Number) {
|
||||
alignVal = peek().text.toInt();
|
||||
}
|
||||
int depth = 1;
|
||||
while (depth > 0 && peek().kind != TokKind::Eof) {
|
||||
if (peek().kind == TokKind::LParen) depth++;
|
||||
else if (peek().kind == TokKind::RParen) depth--;
|
||||
advance();
|
||||
}
|
||||
}
|
||||
return alignVal;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if next tokens after keyword are ALIGN(...) then Ident/LBrace
|
||||
bool peekPastAlign(int offset, TokKind expected) const {
|
||||
int i = cur + offset;
|
||||
if (i < tokens.size() && tokens[i].kind == TokKind::Ident &&
|
||||
(tokens[i].text == QStringLiteral("ALIGN") ||
|
||||
tokens[i].text == QStringLiteral("__declspec"))) {
|
||||
i++; // skip ALIGN
|
||||
if (i < tokens.size() && tokens[i].kind == TokKind::LParen) {
|
||||
int depth = 1; i++;
|
||||
while (i < tokens.size() && depth > 0) {
|
||||
if (tokens[i].kind == TokKind::LParen) depth++;
|
||||
else if (tokens[i].kind == TokKind::RParen) depth--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return i < tokens.size() && tokens[i].kind == expected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Top-level parse ──
|
||||
|
||||
void parse() {
|
||||
while (peek().kind != TokKind::Eof) {
|
||||
if (checkIdent("struct") || checkIdent("class")) {
|
||||
parseStructOrForward();
|
||||
} else if (checkIdent("union")) {
|
||||
parseTopLevelUnion();
|
||||
} else if (checkIdent("static_assert")) {
|
||||
parseStaticAssert();
|
||||
} else if (checkIdent("typedef")) {
|
||||
@@ -400,6 +449,9 @@ struct Parser {
|
||||
void parseStructOrForward() {
|
||||
QString keyword = advance().text; // "struct" or "class"
|
||||
|
||||
// Skip ALIGN( N ) between keyword and name
|
||||
int alignVal = skipAlignMacro();
|
||||
|
||||
// Anonymous struct: struct { ... }
|
||||
if (check(TokKind::LBrace)) {
|
||||
// Skip anonymous struct at top level
|
||||
@@ -411,6 +463,9 @@ struct Parser {
|
||||
if (!check(TokKind::Ident)) { skipToSemiOrBrace(); return; }
|
||||
QString name = advance().text;
|
||||
|
||||
if (alignVal > 0)
|
||||
structAlignments[name] = alignVal;
|
||||
|
||||
// Check for inheritance: struct Foo : public Bar {
|
||||
// Just skip the inheritance clause
|
||||
if (check(TokKind::Colon)) {
|
||||
@@ -446,14 +501,18 @@ struct Parser {
|
||||
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||
// Nested struct definition
|
||||
if (checkIdent("struct") || checkIdent("class")) {
|
||||
if (peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
|
||||
// Check: struct [ALIGN(N)] Name {
|
||||
if ((peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) ||
|
||||
peekPastAlign(1, TokKind::Ident)) {
|
||||
// Nested named struct: parse as a top-level struct, then treat as embedded field
|
||||
parseStructOrForward();
|
||||
continue;
|
||||
}
|
||||
if (peek(1).kind == TokKind::LBrace) {
|
||||
// Check: struct [ALIGN(N)] {
|
||||
if (peek(1).kind == TokKind::LBrace || peekPastAlign(1, TokKind::LBrace)) {
|
||||
// Anonymous nested struct { ... } fieldName;
|
||||
advance(); // skip "struct"
|
||||
skipAlignMacro();
|
||||
advance(); // skip "{"
|
||||
// Skip body
|
||||
int depth = 1;
|
||||
@@ -499,9 +558,54 @@ struct Parser {
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level named union definition: union [ALIGN(N)] Name { ... };
|
||||
// Parsed as a struct with classKeyword "union" and all members as fields
|
||||
void parseTopLevelUnion() {
|
||||
advance(); // skip "union"
|
||||
int alignVal = skipAlignMacro();
|
||||
|
||||
// Forward declaration: union Name;
|
||||
if (check(TokKind::Ident) && peek(1).kind == TokKind::Semi) {
|
||||
QString name = advance().text;
|
||||
advance(); // skip ;
|
||||
forwardDecls.insert(name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Anonymous union at top level (skip)
|
||||
if (check(TokKind::LBrace)) {
|
||||
skipToSemiOrBrace();
|
||||
if (check(TokKind::RBrace)) { advance(); match(TokKind::Semi); }
|
||||
return;
|
||||
}
|
||||
|
||||
if (!check(TokKind::Ident)) { skipToSemiOrBrace(); return; }
|
||||
QString name = advance().text;
|
||||
|
||||
if (alignVal > 0)
|
||||
structAlignments[name] = alignVal;
|
||||
|
||||
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||
|
||||
ParsedStruct ps;
|
||||
ps.name = name;
|
||||
ps.keyword = QStringLiteral("union");
|
||||
|
||||
// Parse body — same as struct body but members overlap at offset 0
|
||||
parseStructBody(ps);
|
||||
|
||||
if (!match(TokKind::RBrace)) { skipToSemiOrBrace(); return; }
|
||||
match(TokKind::Semi);
|
||||
|
||||
structs.append(ps);
|
||||
}
|
||||
|
||||
void parseUnion(ParsedStruct& ps) {
|
||||
advance(); // skip "union"
|
||||
|
||||
// Skip ALIGN( N ) between union keyword and name/brace
|
||||
skipAlignMacro();
|
||||
|
||||
// Optional union tag name (before {)
|
||||
if (check(TokKind::Ident) && peek(1).kind == TokKind::LBrace) {
|
||||
advance(); // skip union tag name
|
||||
@@ -525,9 +629,11 @@ struct Parser {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle anonymous struct inside union: struct { ... };
|
||||
if ((checkIdent("struct") || checkIdent("class")) && peek(1).kind == TokKind::LBrace) {
|
||||
// Handle anonymous struct inside union: struct [ALIGN(N)] { ... };
|
||||
if ((checkIdent("struct") || checkIdent("class")) &&
|
||||
(peek(1).kind == TokKind::LBrace || peekPastAlign(1, TokKind::LBrace))) {
|
||||
advance(); // skip "struct"
|
||||
skipAlignMacro();
|
||||
advance(); // skip "{"
|
||||
int depth = 1;
|
||||
while (peek().kind != TokKind::Eof && depth > 0) {
|
||||
@@ -541,9 +647,10 @@ struct Parser {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle nested named struct definition inside union
|
||||
// Handle nested named struct definition inside union: struct [ALIGN(N)] Name {
|
||||
if ((checkIdent("struct") || checkIdent("class")) &&
|
||||
peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
|
||||
((peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) ||
|
||||
peekPastAlign(1, TokKind::Ident))) {
|
||||
parseStructOrForward();
|
||||
continue;
|
||||
}
|
||||
@@ -584,13 +691,26 @@ struct Parser {
|
||||
QString typeName = parseTypeName();
|
||||
if (typeName.isEmpty()) { cur = startPos; return false; }
|
||||
|
||||
// Resolve typedef
|
||||
while (typedefs.contains(typeName))
|
||||
typeName = typedefs[typeName];
|
||||
// Resolve typedef — track pointer and array typedefs in the chain
|
||||
bool typedefPointer = false;
|
||||
QVector<int> typedefArrayDims;
|
||||
{
|
||||
QString resolved = typeName;
|
||||
QSet<QString> seen;
|
||||
while (typedefs.contains(resolved) && !seen.contains(resolved)) {
|
||||
if (pointerTypedefs.contains(resolved))
|
||||
typedefPointer = true;
|
||||
if (typedefArrayDims.isEmpty() && arrayTypedefs.contains(resolved))
|
||||
typedefArrayDims = arrayTypedefs[resolved];
|
||||
seen.insert(resolved);
|
||||
resolved = typedefs[resolved];
|
||||
}
|
||||
typeName = resolved;
|
||||
}
|
||||
|
||||
// Pointer stars
|
||||
bool isPointer = false;
|
||||
int ptrDepth = 0;
|
||||
bool isPointer = typedefPointer;
|
||||
int ptrDepth = typedefPointer ? 1 : 0;
|
||||
while (match(TokKind::Star)) {
|
||||
isPointer = true;
|
||||
ptrDepth++;
|
||||
@@ -628,6 +748,18 @@ struct Parser {
|
||||
match(TokKind::RBracket);
|
||||
}
|
||||
|
||||
// Apply array dimensions from typedef (e.g. typedef ULONG GDI_HANDLE_BUFFER[60])
|
||||
if (!typedefArrayDims.isEmpty()) {
|
||||
if (field.arraySizes.isEmpty())
|
||||
field.arraySizes = typedefArrayDims;
|
||||
else {
|
||||
// Combine: typedef dims come first, field dims appended
|
||||
QVector<int> combined = typedefArrayDims;
|
||||
combined.append(field.arraySizes);
|
||||
field.arraySizes = combined;
|
||||
}
|
||||
}
|
||||
|
||||
// Bitfield: Type name : width
|
||||
if (check(TokKind::Colon)) {
|
||||
advance();
|
||||
@@ -750,28 +882,83 @@ struct Parser {
|
||||
parseStructOrForward();
|
||||
return;
|
||||
}
|
||||
// typedef struct ExistingName AliasName;
|
||||
// typedef struct ExistingName * AliasName;
|
||||
advance(); // skip struct/class
|
||||
if (check(TokKind::Ident)) {
|
||||
QString existingName = advance().text;
|
||||
// Pointer stars
|
||||
while (match(TokKind::Star)) {}
|
||||
bool hasPtr = false;
|
||||
while (match(TokKind::Star)) { hasPtr = true; }
|
||||
// Skip const/volatile after pointer
|
||||
while (checkIdent("const") || checkIdent("volatile")) advance();
|
||||
if (check(TokKind::Ident)) {
|
||||
QString aliasName = advance().text;
|
||||
typedefs[aliasName] = existingName;
|
||||
if (aliasName != existingName) { // skip self-referencing typedefs
|
||||
typedefs[aliasName] = existingName;
|
||||
if (hasPtr) pointerTypedefs.insert(aliasName);
|
||||
}
|
||||
}
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
return;
|
||||
}
|
||||
|
||||
// typedef BaseType AliasName;
|
||||
// typedef BaseType [*] AliasName [N];
|
||||
// Skip leading const/volatile qualifiers: typedef const Type* Alias;
|
||||
while (checkIdent("const") || checkIdent("volatile")) advance();
|
||||
QString baseType = parseTypeName();
|
||||
if (baseType.isEmpty()) { skipToSemiOrBrace(); return; }
|
||||
while (match(TokKind::Star)) {} // pointer typedefs
|
||||
bool hasPtr = false;
|
||||
while (match(TokKind::Star)) { hasPtr = true; }
|
||||
// Skip const/volatile after pointer
|
||||
while (checkIdent("const") || checkIdent("volatile")) advance();
|
||||
while (match(TokKind::Star)) { hasPtr = true; }
|
||||
|
||||
// Function pointer typedef: typedef RetType ( *Name )( args... );
|
||||
if (check(TokKind::LParen)) {
|
||||
int save = cur;
|
||||
advance(); // skip (
|
||||
bool isFnPtr = false;
|
||||
QString fnName;
|
||||
if (match(TokKind::Star) && check(TokKind::Ident)) {
|
||||
fnName = advance().text;
|
||||
if (match(TokKind::RParen) && check(TokKind::LParen)) {
|
||||
isFnPtr = true;
|
||||
}
|
||||
}
|
||||
if (isFnPtr) {
|
||||
// Skip the argument list and register as pointer type
|
||||
skipToSemiOrBrace();
|
||||
pointerTypedefs.insert(fnName);
|
||||
typedefs[fnName] = QStringLiteral("void");
|
||||
} else {
|
||||
cur = save;
|
||||
skipToSemiOrBrace();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (check(TokKind::Ident)) {
|
||||
QString alias = advance().text;
|
||||
typedefs[alias] = baseType;
|
||||
// Array dimensions: typedef Type Name[N][M];
|
||||
QVector<int> dims;
|
||||
while (check(TokKind::LBracket)) {
|
||||
advance();
|
||||
if (check(TokKind::Number)) {
|
||||
bool ok;
|
||||
QString numText = peek().text;
|
||||
int val = numText.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)
|
||||
? numText.mid(2).toInt(&ok, 16) : numText.toInt(&ok);
|
||||
if (ok) dims.append(val);
|
||||
advance();
|
||||
}
|
||||
match(TokKind::RBracket);
|
||||
}
|
||||
if (alias != baseType) { // skip self-referencing typedefs
|
||||
typedefs[alias] = baseType;
|
||||
if (hasPtr) pointerTypedefs.insert(alias);
|
||||
if (!dims.isEmpty()) arrayTypedefs[alias] = dims;
|
||||
}
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
}
|
||||
@@ -847,7 +1034,7 @@ struct Parser {
|
||||
}
|
||||
}
|
||||
|
||||
ps.enumValues.append({memberName, memberValue});
|
||||
ps.enumValues.emplaceBack(memberName, memberValue);
|
||||
nextValue = memberValue + 1;
|
||||
|
||||
// Skip comma between members
|
||||
@@ -945,8 +1132,72 @@ struct BuildContext {
|
||||
bool useCommentOffsets;
|
||||
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
|
||||
int ptrSize = 8; // target pointer size (4 or 8)
|
||||
const QHash<QString, int>& sizeAsserts; // declared struct sizes from static_assert
|
||||
const QHash<QString, int>& structAlignments; // struct name -> ALIGN(N) value
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
static int fieldNaturalAlignment(const ParsedField& field, const BuildContext& ctx);
|
||||
|
||||
// Compute natural alignment for a union from its members (max member alignment)
|
||||
static int unionNaturalAlignment(const ParsedField& field, const BuildContext& ctx) {
|
||||
int maxAlign = 1;
|
||||
for (const auto& member : field.unionMembers) {
|
||||
int a = fieldNaturalAlignment(member, ctx);
|
||||
if (a > maxAlign) maxAlign = a;
|
||||
}
|
||||
return maxAlign;
|
||||
}
|
||||
|
||||
// Return natural alignment for a parsed field (used when computing offsets without comments)
|
||||
static int fieldNaturalAlignment(const ParsedField& field, const BuildContext& ctx) {
|
||||
if (field.isPointer) return ctx.ptrSize;
|
||||
if (field.isUnion) return unionNaturalAlignment(field, ctx);
|
||||
if (field.bitfieldWidth >= 0) {
|
||||
// Bitfield alignment is determined by its storage type
|
||||
auto it = ctx.typeTable.find(field.typeName);
|
||||
if (it != ctx.typeTable.end()) return alignmentFor(it->kind);
|
||||
return 4; // default bitfield alignment
|
||||
}
|
||||
auto it = ctx.typeTable.find(field.typeName);
|
||||
if (it != ctx.typeTable.end()) return alignmentFor(it->kind);
|
||||
// Unknown type (struct reference) — align to pointer size
|
||||
return ctx.ptrSize;
|
||||
}
|
||||
|
||||
static inline int alignUp(int offset, int align) {
|
||||
return (offset + align - 1) & ~(align - 1);
|
||||
}
|
||||
|
||||
// Look up the byte size of a struct type (from already-built tree or static_assert declarations)
|
||||
static int structTypeSize(const QString& typeName, const BuildContext& ctx) {
|
||||
auto classIt = ctx.classIds.find(typeName);
|
||||
if (classIt != ctx.classIds.end()) {
|
||||
int span = ctx.tree.structSpan(classIt.value());
|
||||
if (span > 0) {
|
||||
// Pad to struct's declared alignment (ALIGN(N))
|
||||
auto alignIt = ctx.structAlignments.find(typeName);
|
||||
if (alignIt != ctx.structAlignments.end() && *alignIt > 1)
|
||||
span = alignUp(span, *alignIt);
|
||||
return span;
|
||||
}
|
||||
}
|
||||
auto sizeIt = ctx.sizeAsserts.find(typeName);
|
||||
if (sizeIt != ctx.sizeAsserts.end())
|
||||
return sizeIt.value();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Compute total array elements from multi-dimensional sizes, capped to prevent overflow.
|
||||
static int clampedArrayElements(const QVector<int>& dims, int maxElements = 1000000) {
|
||||
int64_t total = 1;
|
||||
for (int dim : dims) {
|
||||
total *= (dim > 0 ? dim : 1);
|
||||
if (total > maxElements) return maxElements;
|
||||
}
|
||||
return (int)total;
|
||||
}
|
||||
|
||||
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
const QVector<ParsedField>& fields) {
|
||||
int computedOffset = 0;
|
||||
@@ -959,8 +1210,11 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int groupOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
groupOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
else {
|
||||
int bfAlign = fieldNaturalAlignment(field, ctx);
|
||||
computedOffset = alignUp(computedOffset, bfAlign);
|
||||
groupOffset = computedOffset;
|
||||
}
|
||||
int startIdx = fi;
|
||||
int totalBits = 0;
|
||||
while (fi < fields.size() && fields[fi].bitfieldWidth >= 0) {
|
||||
@@ -982,8 +1236,11 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int unionOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
unionOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
else {
|
||||
int uAlign = fieldNaturalAlignment(field, ctx);
|
||||
computedOffset = alignUp(computedOffset, uAlign);
|
||||
unionOffset = computedOffset;
|
||||
}
|
||||
|
||||
Node unionNode;
|
||||
unionNode.kind = NodeKind::Struct;
|
||||
@@ -1013,8 +1270,11 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int fieldOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
fieldOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
else {
|
||||
int fAlign = fieldNaturalAlignment(field, ctx);
|
||||
computedOffset = alignUp(computedOffset, fAlign);
|
||||
fieldOffset = computedOffset;
|
||||
}
|
||||
|
||||
// Resolve type
|
||||
auto typeIt = ctx.typeTable.find(field.typeName);
|
||||
@@ -1022,8 +1282,26 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
// Pointer field
|
||||
if (field.isPointer) {
|
||||
NodeKind ptrKind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
|
||||
// Array of pointers: PVOID arr[N]
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = ptrKind;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * ctx.ptrSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
Node n;
|
||||
n.kind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
n.kind = ptrKind;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
@@ -1034,7 +1312,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
if (!field.pointerTarget.isEmpty() &&
|
||||
field.pointerTarget != QStringLiteral("void")) {
|
||||
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
||||
ctx.pendingRefs.push_back(PendingRef{nodeId, field.pointerTarget});
|
||||
}
|
||||
|
||||
computedOffset = fieldOffset + ctx.ptrSize;
|
||||
@@ -1046,8 +1324,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int elemSize = 4;
|
||||
NodeKind elemKind = NodeKind::UInt32;
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
@@ -1065,7 +1342,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
n.offset = fieldOffset;
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
ctx.pendingRefs.push_back(PendingRef{nodeId, field.typeName});
|
||||
computedOffset = fieldOffset + elemSize;
|
||||
}
|
||||
continue;
|
||||
@@ -1098,7 +1375,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
if (firstDim <= 0) firstDim = 1;
|
||||
|
||||
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
||||
field.typeName == QStringLiteral("char")) {
|
||||
(field.typeName == QStringLiteral("char") ||
|
||||
field.typeName == QStringLiteral("CHAR"))) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF8;
|
||||
n.name = field.name;
|
||||
@@ -1111,7 +1389,9 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
||||
(field.typeName == QStringLiteral("wchar_t") ||
|
||||
field.typeName == QStringLiteral("WCHAR") ||
|
||||
field.typeName == QStringLiteral("TCHAR"))) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF16;
|
||||
n.name = field.name;
|
||||
@@ -1148,8 +1428,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
|
||||
}
|
||||
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
@@ -1165,9 +1444,10 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
// Struct-type field
|
||||
if (isStructType) {
|
||||
int elemSize = structTypeSize(field.typeName, ctx);
|
||||
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
@@ -1181,7 +1461,9 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
ctx.pendingRefs.push_back(PendingRef{nodeId, field.typeName});
|
||||
if (elemSize > 0)
|
||||
computedOffset = fieldOffset + totalElements * elemSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1195,7 +1477,9 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
ctx.pendingRefs.push_back(PendingRef{nodeId, field.typeName});
|
||||
if (elemSize > 0)
|
||||
computedOffset = fieldOffset + elemSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1271,7 +1555,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int poin
|
||||
enumNames.insert(ps.name);
|
||||
}
|
||||
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames, pointerSize};
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames, pointerSize, parser.sizeAsserts, parser.structAlignments};
|
||||
|
||||
// Build nodes for each struct/enum
|
||||
for (const auto& ps : parser.structs) {
|
||||
@@ -1300,6 +1584,13 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int poin
|
||||
|
||||
buildFields(ctx, structId, 0, ps.fields);
|
||||
|
||||
// Union: all direct children overlap at offset 0
|
||||
if (ps.keyword == QStringLiteral("union")) {
|
||||
QVector<int> children = tree.childrenOf(structId);
|
||||
for (int ci : children)
|
||||
tree.nodes[ci].offset = 0;
|
||||
}
|
||||
|
||||
// Apply static_assert size: add tail padding if needed
|
||||
auto sizeIt = parser.sizeAsserts.find(ps.name);
|
||||
if (sizeIt != parser.sizeAsserts.end()) {
|
||||
|
||||
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
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <cstdint>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class Provider;
|
||||
|
||||
struct PdbDebugInfo {
|
||||
QString pdbName; // e.g. "ntoskrnl.pdb"
|
||||
QString guidString; // 32 hex chars, no dashes, uppercase
|
||||
uint32_t age = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
// Extract PDB debug info (GUID, age, filename) from a PE module in memory.
|
||||
// Reads DOS header → PE header → debug directory → CodeView RSDS record.
|
||||
PdbDebugInfo extractPdbDebugInfo(const Provider& prov, uint64_t moduleBase);
|
||||
|
||||
} // namespace rcx
|
||||
@@ -10,8 +10,9 @@
|
||||
#define RCX_PLUGIN_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
// Forward declaration
|
||||
// Forward declarations
|
||||
namespace rcx { class Provider; }
|
||||
class QMenu;
|
||||
|
||||
/**
|
||||
* Plugin interface for Reclass
|
||||
@@ -129,6 +130,13 @@ public:
|
||||
* @return true if enumerateProcesses() should be called
|
||||
*/
|
||||
virtual bool providesProcessList() const { return false; }
|
||||
|
||||
/**
|
||||
* Add plugin-specific actions to the source menu (optional).
|
||||
* Called each time the source menu is shown. Only add items when relevant
|
||||
* (e.g., "Unload Driver" only when the driver is loaded).
|
||||
*/
|
||||
virtual void populatePluginMenu(QMenu*) {}
|
||||
};
|
||||
|
||||
// Plugin factory function signature
|
||||
|
||||
13
src/macos_titlebar.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct Theme;
|
||||
|
||||
// Apply macOS native title bar color to match the theme.
|
||||
// No-op on non-macOS platforms (implementation is platform-specific).
|
||||
void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
|
||||
|
||||
} // namespace rcx
|
||||
43
src/macos_titlebar.mm
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "macos_titlebar.h"
|
||||
#include "themes/theme.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <QColor>
|
||||
#include <QWidget>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
static NSColor* toNSColor(const QColor& color) {
|
||||
return [NSColor colorWithCalibratedRed:color.redF()
|
||||
green:color.greenF()
|
||||
blue:color.blueF()
|
||||
alpha:color.alphaF()];
|
||||
}
|
||||
|
||||
void applyMacTitleBarTheme(QWidget* window, const Theme& theme) {
|
||||
if (!window) return;
|
||||
|
||||
// Ensure native window is created.
|
||||
window->winId();
|
||||
|
||||
auto* nsView = reinterpret_cast<NSView*>(window->winId());
|
||||
if (!nsView) return;
|
||||
|
||||
NSWindow* nsWindow = [nsView window];
|
||||
if (!nsWindow) return;
|
||||
|
||||
// Keep native traffic lights while tinting the title bar to the theme.
|
||||
// Match the title text contrast by selecting the appropriate system appearance.
|
||||
const qreal luminance =
|
||||
0.2126 * theme.background.redF() +
|
||||
0.7152 * theme.background.greenF() +
|
||||
0.0722 * theme.background.blueF();
|
||||
const bool isLight = luminance >= 0.5;
|
||||
[nsWindow setAppearance:[NSAppearance appearanceNamed:
|
||||
(isLight ? NSAppearanceNameAqua : NSAppearanceNameDarkAqua)]];
|
||||
[nsWindow setTitlebarAppearsTransparent:YES];
|
||||
[nsWindow setTitleVisibility:NSWindowTitleVisible];
|
||||
[nsWindow setBackgroundColor:toNSColor(theme.background)];
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
4004
src/main.cpp
@@ -3,9 +3,10 @@
|
||||
#include "titlebar.h"
|
||||
#include "pluginmanager.h"
|
||||
#include "scannerpanel.h"
|
||||
#include "startpage.h"
|
||||
#include "workspace_model.h"
|
||||
namespace rcx { class SymbolDownloader; }
|
||||
#include <QMainWindow>
|
||||
#include <QMdiArea>
|
||||
#include <QMdiSubWindow>
|
||||
#include <QLabel>
|
||||
#include <QSplitter>
|
||||
#include <QTabWidget>
|
||||
@@ -16,14 +17,18 @@
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
#include <QButtonGroup>
|
||||
#include <QComboBox>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class McpBridge;
|
||||
class ShimmerLabel;
|
||||
class DockGripWidget;
|
||||
class WorkspaceDelegate;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -56,6 +61,10 @@ private slots:
|
||||
void toggleMcp();
|
||||
void setEditorFont(const QString& fontName);
|
||||
void exportCpp();
|
||||
void exportRust();
|
||||
void exportDefines();
|
||||
void exportCSharp();
|
||||
void exportPython();
|
||||
void exportReclassXmlAction();
|
||||
void importFromSource();
|
||||
void importReclassXml();
|
||||
@@ -63,43 +72,51 @@ private slots:
|
||||
void showTypeAliasesDialog();
|
||||
void editTheme();
|
||||
void showOptionsDialog();
|
||||
void showOptionsDialog(int initialPage);
|
||||
|
||||
public:
|
||||
// Status bar helpers — separate app / MCP channels
|
||||
void setAppStatus(const QString& text);
|
||||
void setAppStatus(const QString& text, const QString& dimSuffix);
|
||||
void setMcpStatus(const QString& text);
|
||||
void clearMcpStatus();
|
||||
|
||||
// Project Lifecycle API
|
||||
QMdiSubWindow* project_new(const QString& classKeyword = QString());
|
||||
QMdiSubWindow* project_open(const QString& path = {});
|
||||
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
|
||||
void project_close(QMdiSubWindow* sub = nullptr);
|
||||
QDockWidget* project_new(const QString& classKeyword = QString());
|
||||
QDockWidget* project_open(const QString& path = {});
|
||||
bool project_save(QDockWidget* dock = nullptr, bool saveAs = false);
|
||||
void project_close(QDockWidget* dock = nullptr);
|
||||
|
||||
private:
|
||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||
|
||||
QMdiArea* m_mdiArea;
|
||||
QWidget* m_centralPlaceholder;
|
||||
ShimmerLabel* m_statusLabel;
|
||||
QString m_appStatus;
|
||||
QString m_appStatusDim;
|
||||
bool m_mcpBusy = false;
|
||||
QTimer* m_mcpClearTimer = nullptr;
|
||||
QButtonGroup* m_viewBtnGroup = nullptr;
|
||||
QPushButton* m_btnReclass = nullptr;
|
||||
QPushButton* m_btnRendered = nullptr;
|
||||
TitleBarWidget* m_titleBar = nullptr;
|
||||
QMenuBar* m_menuBar = nullptr;
|
||||
bool m_menuBarTitleCase = false;
|
||||
QWidget* m_borderOverlay = nullptr;
|
||||
PluginManager m_pluginManager;
|
||||
McpBridge* m_mcp = nullptr;
|
||||
QAction* m_mcpAction = nullptr;
|
||||
QAction* m_actRelOfs = nullptr;
|
||||
QMenu* m_sourceMenu = nullptr;
|
||||
QMenu* m_recentFilesMenu = nullptr;
|
||||
|
||||
struct SplitPane {
|
||||
QTabWidget* tabWidget = nullptr;
|
||||
RcxEditor* editor = nullptr;
|
||||
QsciScintilla* rendered = nullptr;
|
||||
QLineEdit* findBar = nullptr;
|
||||
QWidget* findContainer = nullptr;
|
||||
QWidget* renderedContainer = nullptr;
|
||||
QComboBox* fmtCombo = nullptr;
|
||||
QComboBox* scopeCombo = nullptr;
|
||||
QToolButton* fmtGear = nullptr;
|
||||
ViewMode viewMode = VM_Reclass;
|
||||
uint64_t lastRenderedRootId = 0;
|
||||
};
|
||||
@@ -111,22 +128,38 @@ private:
|
||||
QVector<SplitPane> panes;
|
||||
int activePaneIdx = 0;
|
||||
};
|
||||
QMap<QMdiSubWindow*, TabState> m_tabs;
|
||||
QMap<QDockWidget*, TabState> m_tabs;
|
||||
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
|
||||
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
|
||||
QVector<QDockWidget*> m_sentinelDocks; // permanent sentinels for always-visible tab bars
|
||||
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
||||
bool m_closingAll = false; // guards spurious project_new during batch close
|
||||
struct ClosingGuard {
|
||||
bool& flag;
|
||||
ClosingGuard(bool& f) : flag(f) { flag = true; }
|
||||
~ClosingGuard() { flag = false; }
|
||||
};
|
||||
void rebuildAllDocs();
|
||||
|
||||
void createMenus();
|
||||
void applyMenuBarTitleCase(bool titleCase);
|
||||
void createStatusBar();
|
||||
void showPluginsDialog();
|
||||
void populateSourceMenu();
|
||||
void addRecentFile(const QString& path);
|
||||
void updateRecentFilesMenu();
|
||||
QIcon makeIcon(const QString& svgPath);
|
||||
|
||||
RcxController* activeController() const;
|
||||
TabState* activeTab();
|
||||
TabState* tabByIndex(int index);
|
||||
int tabCount() const { return m_tabs.size(); }
|
||||
QMdiSubWindow* createTab(RcxDocument* doc);
|
||||
QDockWidget* createSentinelDock();
|
||||
QDockWidget* createTab(RcxDocument* doc);
|
||||
QString tabTitle(const TabState& tab) const;
|
||||
void setupDockTabBars();
|
||||
void updateWindowTitle();
|
||||
void closeAllDocDocks();
|
||||
|
||||
void setViewMode(ViewMode mode);
|
||||
void updateRenderedView(TabState& tab, SplitPane& pane);
|
||||
@@ -136,7 +169,6 @@ private:
|
||||
|
||||
SplitPane createSplitPane(TabState& tab);
|
||||
void applyTheme(const Theme& theme);
|
||||
void styleTabCloseButtons();
|
||||
void syncViewButtons(ViewMode mode);
|
||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||
SplitPane* findActiveSplitPane();
|
||||
@@ -148,10 +180,16 @@ private:
|
||||
QStandardItemModel* m_workspaceModel = nullptr;
|
||||
QSortFilterProxyModel* m_workspaceProxy = nullptr;
|
||||
QLineEdit* m_workspaceSearch = nullptr;
|
||||
WorkspaceDelegate* m_workspaceDelegate = nullptr;
|
||||
QLabel* m_dockTitleLabel = nullptr;
|
||||
QToolButton* m_dockCloseBtn = nullptr;
|
||||
DockGripWidget* m_dockGrip = nullptr;
|
||||
QSet<uint64_t> m_pinnedIds;
|
||||
void createWorkspaceDock();
|
||||
void rebuildWorkspaceModel();
|
||||
void rebuildWorkspaceModel(); // debounced — safe to call frequently
|
||||
void rebuildWorkspaceModelNow(); // immediate rebuild
|
||||
QTimer* m_workspaceRebuildTimer = nullptr;
|
||||
QTimer* m_workspaceSearchTimer = nullptr;
|
||||
void updateBorderColor(const QColor& color);
|
||||
|
||||
// Scanner dock
|
||||
@@ -159,11 +197,42 @@ private:
|
||||
ScannerPanel* m_scannerPanel = nullptr;
|
||||
QLabel* m_scanDockTitle = nullptr;
|
||||
QToolButton* m_scanDockCloseBtn = nullptr;
|
||||
DockGripWidget* m_scanDockGrip = nullptr;
|
||||
void createScannerDock();
|
||||
|
||||
// Modules/Symbols dock
|
||||
QDockWidget* m_symbolsDock = nullptr;
|
||||
QTabWidget* m_symTabWidget = nullptr;
|
||||
// Modules tab
|
||||
QTreeView* m_modulesTree = nullptr;
|
||||
QStandardItemModel* m_modulesModel = nullptr;
|
||||
// Symbols tab
|
||||
QTreeView* m_symbolsTree = nullptr;
|
||||
QStandardItemModel* m_symbolsModel = nullptr;
|
||||
QSortFilterProxyModel* m_symbolsProxy = nullptr;
|
||||
QLineEdit* m_symbolsSearch = nullptr;
|
||||
// Title bar
|
||||
QLabel* m_symDockTitle = nullptr;
|
||||
QToolButton* m_symDockCloseBtn = nullptr;
|
||||
QToolButton* m_symDownloadBtn = nullptr;
|
||||
DockGripWidget* m_symDockGrip = nullptr;
|
||||
rcx::SymbolDownloader* m_symDownloader = nullptr;
|
||||
void createSymbolsDock();
|
||||
void rebuildSymbolsModel();
|
||||
void rebuildModulesModel();
|
||||
void downloadSymbolsForProcess();
|
||||
// Load PDB symbols + typeIndices into SymbolStore. Returns symbol count.
|
||||
static int loadPdbIntoStore(const QString& pdbPath);
|
||||
|
||||
// Start page
|
||||
StartPageWidget* m_startPage = nullptr;
|
||||
Q_INVOKABLE void showStartPage();
|
||||
void dismissStartPage();
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QByteArray>
|
||||
#include <QTimer>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -28,12 +29,31 @@ public:
|
||||
void notifyDataChanged();
|
||||
|
||||
private:
|
||||
struct ClientState {
|
||||
QLocalSocket* socket = nullptr;
|
||||
QByteArray readBuffer;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
MainWindow* m_mainWindow;
|
||||
QLocalServer* m_server = nullptr;
|
||||
QLocalSocket* m_client = nullptr; // single client for v1
|
||||
QByteArray m_readBuffer;
|
||||
bool m_initialized = false;
|
||||
QVector<ClientState> m_clients;
|
||||
QLocalSocket* m_currentSender = nullptr; // set during request processing
|
||||
bool m_slowMode = false;
|
||||
QTimer* m_notifyTimer = nullptr;
|
||||
|
||||
// Serial request queue. Some tool calls (scanner, tree.apply) spin nested
|
||||
// event loops which would let another client's readyRead interleave and
|
||||
// clobber m_currentSender. Simplest fix without refactoring those tools:
|
||||
// queue incoming lines while a request is in flight, drain after.
|
||||
bool m_processing = false;
|
||||
struct PendingRequest { QLocalSocket* socket; QByteArray line; };
|
||||
QVector<PendingRequest> m_pendingRequests;
|
||||
|
||||
|
||||
ClientState* findClient(QLocalSocket* sock);
|
||||
void removeClient(QLocalSocket* sock);
|
||||
void drainPendingRequests();
|
||||
|
||||
// JSON-RPC plumbing
|
||||
void onNewConnection();
|
||||
@@ -54,19 +74,30 @@ private:
|
||||
QJsonObject toolProjectState(const QJsonObject& args);
|
||||
QJsonObject toolTreeApply(const QJsonObject& args);
|
||||
QJsonObject toolSourceSwitch(const QJsonObject& args);
|
||||
QJsonObject toolSourceModules(const QJsonObject& args);
|
||||
QJsonObject toolHexRead(const QJsonObject& args);
|
||||
QJsonObject toolHexWrite(const QJsonObject& args);
|
||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
QJsonObject toolNodeHistory(const QJsonObject& args);
|
||||
QJsonObject toolScannerScan(const QJsonObject& args);
|
||||
QJsonObject toolScannerScanPattern(const QJsonObject& args);
|
||||
QJsonObject toolReconnect(const QJsonObject& args);
|
||||
QJsonObject toolProcessInfo(const QJsonObject& args);
|
||||
QJsonObject toolSymbolsLoad(const QJsonObject& args);
|
||||
QJsonObject toolSymbolsLookup(const QJsonObject& args);
|
||||
QJsonObject toolSymbolsImportType(const QJsonObject& args);
|
||||
QJsonObject toolNodeReadValue(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
QString resolvePlaceholder(const QString& ref,
|
||||
const QHash<QString, uint64_t>& placeholderMap);
|
||||
const QHash<QString, uint64_t>& placeholderMap,
|
||||
bool* ok = nullptr);
|
||||
|
||||
// Smart tab resolution: tabIndex arg → activeTab → first tab → auto-create
|
||||
MainWindow::TabState* resolveTab(const QJsonObject& args);
|
||||
MainWindow::TabState* resolveTab(const QJsonObject& args, int* resolvedIndex = nullptr);
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -40,9 +40,21 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
m_tree->setHeaderHidden(true);
|
||||
m_tree->setRootIsDecorated(true);
|
||||
m_tree->setFixedWidth(200);
|
||||
m_tree->setMouseTracking(true);
|
||||
m_tree->setIconSize(QSize(16, 16));
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
QPalette tp = m_tree->palette();
|
||||
tp.setColor(QPalette::Text, t.textDim);
|
||||
tp.setColor(QPalette::Highlight, t.hover);
|
||||
tp.setColor(QPalette::HighlightedText, t.text);
|
||||
m_tree->setPalette(tp);
|
||||
}
|
||||
|
||||
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
|
||||
envItem->setIcon(0, QIcon(":/vsicons/folder.svg"));
|
||||
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
|
||||
generalItem->setIcon(0, QIcon(":/vsicons/settings-gear.svg"));
|
||||
m_tree->expandAll();
|
||||
m_tree->setCurrentItem(generalItem);
|
||||
leftColumn->addWidget(m_tree, 1);
|
||||
@@ -102,7 +114,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
m_fontCombo->setObjectName("fontCombo");
|
||||
visualLayout->addRow("Editor Font:", m_fontCombo);
|
||||
|
||||
m_titleCaseCheck = new QCheckBox("Apply title case styling to menu bar");
|
||||
m_titleCaseCheck = new QCheckBox("Uppercase menu items");
|
||||
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
|
||||
visualLayout->addRow(m_titleCaseCheck);
|
||||
|
||||
@@ -110,25 +122,11 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
m_showIconCheck->setChecked(current.showIcon);
|
||||
visualLayout->addRow(m_showIconCheck);
|
||||
|
||||
m_braceWrapCheck = new QCheckBox("Opening brace on new line");
|
||||
m_braceWrapCheck->setChecked(current.braceWrap);
|
||||
visualLayout->addRow(m_braceWrapCheck);
|
||||
|
||||
generalLayout->addWidget(visualGroup);
|
||||
|
||||
// Safe Mode group box
|
||||
auto* safeModeGroup = new QGroupBox("Preview Features");
|
||||
auto* safeModeLayout = new QVBoxLayout(safeModeGroup);
|
||||
safeModeLayout->setSpacing(4);
|
||||
|
||||
m_safeModeCheck = new QCheckBox("Safe Mode");
|
||||
m_safeModeCheck->setChecked(current.safeMode);
|
||||
safeModeLayout->addWidget(m_safeModeCheck);
|
||||
|
||||
auto* safeModeDesc = new QLabel(
|
||||
"Enable to use the default OS icon for this application and "
|
||||
"create the window with the name of the executable file.");
|
||||
safeModeDesc->setWordWrap(true);
|
||||
safeModeDesc->setContentsMargins(20, 0, 0, 0); // indent under checkbox
|
||||
safeModeLayout->addWidget(safeModeDesc);
|
||||
|
||||
generalLayout->addWidget(safeModeGroup);
|
||||
generalLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(generalPage); // index 0
|
||||
@@ -136,6 +134,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
|
||||
// -- AI Features page --
|
||||
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
|
||||
aiItem->setIcon(0, QIcon(":/vsicons/remote.svg"));
|
||||
|
||||
auto* aiPage = new QWidget;
|
||||
auto* aiLayout = new QVBoxLayout(aiPage);
|
||||
@@ -165,6 +164,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
|
||||
// -- Generator page --
|
||||
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
|
||||
generatorItem->setIcon(0, QIcon(":/vsicons/code.svg"));
|
||||
|
||||
auto* generatorPage = new QWidget;
|
||||
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
||||
@@ -207,16 +207,26 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
|
||||
}
|
||||
|
||||
void OptionsDialog::selectPage(int index) {
|
||||
for (auto it = m_itemPageIndex.begin(); it != m_itemPageIndex.end(); ++it) {
|
||||
if (it.value() == index) {
|
||||
m_tree->setCurrentItem(it.key());
|
||||
m_pages->setCurrentIndex(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OptionsResult OptionsDialog::result() const {
|
||||
OptionsResult r;
|
||||
r.themeIndex = m_themeCombo->currentIndex();
|
||||
r.fontName = m_fontCombo->currentText();
|
||||
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
|
||||
r.showIcon = m_showIconCheck->isChecked();
|
||||
r.safeMode = m_safeModeCheck->isChecked();
|
||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||
r.refreshMs = m_refreshSpin->value();
|
||||
r.generatorAsserts = m_assertCheck->isChecked();
|
||||
r.braceWrap = m_braceWrapCheck->isChecked();
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ struct OptionsResult {
|
||||
QString fontName;
|
||||
bool menuBarTitleCase = true;
|
||||
bool showIcon = false;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = true;
|
||||
int refreshMs = 660;
|
||||
bool generatorAsserts = false;
|
||||
bool braceWrap = false;
|
||||
};
|
||||
|
||||
class OptionsDialog : public QDialog {
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
explicit OptionsDialog(const OptionsResult& current, QWidget* parent = nullptr);
|
||||
|
||||
OptionsResult result() const;
|
||||
void selectPage(int index);
|
||||
|
||||
private:
|
||||
void filterTree(const QString& text);
|
||||
@@ -39,10 +40,10 @@ private:
|
||||
QComboBox* m_fontCombo = nullptr;
|
||||
QCheckBox* m_titleCaseCheck = nullptr;
|
||||
QCheckBox* m_showIconCheck = nullptr;
|
||||
QCheckBox* m_safeModeCheck = nullptr;
|
||||
QCheckBox* m_autoMcpCheck = nullptr;
|
||||
QSpinBox* m_refreshSpin = nullptr;
|
||||
QCheckBox* m_assertCheck = nullptr;
|
||||
QCheckBox* m_braceWrapCheck = nullptr;
|
||||
|
||||
// searchable keywords per leaf tree item
|
||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||
|
||||
@@ -12,17 +12,16 @@ PluginManager::~PluginManager()
|
||||
|
||||
void PluginManager::LoadPlugins()
|
||||
{
|
||||
// Get the Plugins directory relative to the executable
|
||||
// Probe plugin locations relative to the executable.
|
||||
QString appDir = QCoreApplication::applicationDirPath();
|
||||
QString pluginsDir = appDir + "/Plugins";
|
||||
|
||||
QDir dir(pluginsDir);
|
||||
if (!dir.exists())
|
||||
{
|
||||
qWarning() << "PluginManager: Plugins directory not found:" << pluginsDir;
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList pluginDirs;
|
||||
pluginDirs << (appDir + "/Plugins");
|
||||
#ifdef __APPLE__
|
||||
// In macOS app bundles, plugins may live in Contents/PlugIns or in
|
||||
// the top-level build/Plugins directory during local development.
|
||||
pluginDirs << QDir::cleanPath(appDir + "/../PlugIns");
|
||||
#endif
|
||||
|
||||
// Find all DLL files
|
||||
QStringList filters;
|
||||
#ifdef _WIN32
|
||||
@@ -32,17 +31,36 @@ void PluginManager::LoadPlugins()
|
||||
#else
|
||||
filters << "*.so";
|
||||
#endif
|
||||
|
||||
dir.setNameFilters(filters);
|
||||
QFileInfoList files = dir.entryInfoList(QDir::Files);
|
||||
|
||||
qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir;
|
||||
qDebug() << "PluginManager: Found" << files.count() << "potential plugin(s)";
|
||||
|
||||
for (const QFileInfo& fileInfo : files)
|
||||
|
||||
int totalCandidates = 0;
|
||||
bool foundAnyDir = false;
|
||||
for (const QString& pluginsDir : pluginDirs)
|
||||
{
|
||||
LoadPlugin(fileInfo.absoluteFilePath());
|
||||
QDir dir(pluginsDir);
|
||||
if (!dir.exists())
|
||||
continue;
|
||||
|
||||
foundAnyDir = true;
|
||||
dir.setNameFilters(filters);
|
||||
QFileInfoList files = dir.entryInfoList(QDir::Files);
|
||||
totalCandidates += files.count();
|
||||
|
||||
qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundAnyDir)
|
||||
qWarning() << "PluginManager: Plugins directory not found. Searched:" << pluginDirs;
|
||||
else
|
||||
qDebug() << "PluginManager: Found" << totalCandidates << "potential plugin(s)";
|
||||
|
||||
qDebug() << "PluginManager: Loaded" << m_plugins.count() << "plugin(s)";
|
||||
}
|
||||
@@ -83,7 +101,7 @@ bool PluginManager::LoadPlugin(const QString& path)
|
||||
qDebug() << "PluginManager: Loaded plugin:" << plugin->Name().c_str() << plugin->Version().c_str() << "by" << plugin->Author().c_str();
|
||||
|
||||
// Store plugin entry
|
||||
m_entries.append({library, plugin});
|
||||
m_entries.push_back(PluginEntry{library, plugin});
|
||||
m_plugins.append(plugin);
|
||||
|
||||
// Auto-register providers in global registry
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <QMessageBox>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QMenu>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -27,22 +31,9 @@ ProcessPicker::ProcessPicker(QWidget *parent)
|
||||
, m_useCustomList(false)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column - fixed width
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column - fixed width
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true); // Path column - fills remaining space
|
||||
ui->processTable->setWordWrap(false); // Disable word wrap for single-line display
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft); // Elide from left (show end of path)
|
||||
|
||||
// Connect signals
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Initial process enumeration
|
||||
initUi();
|
||||
refreshProcessList();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget *parent)
|
||||
@@ -51,23 +42,103 @@ ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget
|
||||
, m_useCustomList(true)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80);
|
||||
ui->processTable->setColumnWidth(1, 200);
|
||||
initUi();
|
||||
ui->refreshButton->setVisible(false);
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
void ProcessPicker::initUi()
|
||||
{
|
||||
// Table configuration
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true);
|
||||
ui->processTable->setSortingEnabled(true);
|
||||
ui->processTable->setWordWrap(false);
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft);
|
||||
|
||||
// Connect signals (no refresh button for custom lists)
|
||||
ui->refreshButton->setVisible(false);
|
||||
ui->processTable->setShowGrid(false);
|
||||
ui->processTable->verticalHeader()->setDefaultSectionSize(fontMetrics().height() + 6);
|
||||
|
||||
// Signal connections
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Use custom process list
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
|
||||
// Derive theme colors from the global palette (set by applyGlobalTheme)
|
||||
QPalette pal = qApp->palette();
|
||||
QString bg = pal.color(QPalette::Base).name();
|
||||
QString text = pal.color(QPalette::Text).name();
|
||||
QString hover = pal.color(QPalette::Mid).name();
|
||||
QString surface = pal.color(QPalette::AlternateBase).name();
|
||||
QString button = pal.color(QPalette::Button).name();
|
||||
QString highlight= pal.color(QPalette::Highlight).name();
|
||||
QString border = pal.color(QPalette::Mid).darker(120).name();
|
||||
QString mutedText= pal.color(QPalette::Disabled, QPalette::WindowText).name();
|
||||
QString hoverDk = pal.color(QPalette::Mid).darker(130).name();
|
||||
|
||||
ui->processTable->setStyleSheet(QStringLiteral(
|
||||
"QTableWidget { background: %1; color: %2; border: none; }"
|
||||
"QTableWidget::item { padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }")
|
||||
.arg(bg, text, hover));
|
||||
|
||||
ui->processTable->horizontalHeader()->setStyleSheet(QStringLiteral(
|
||||
"QHeaderView::section { background: %1; color: %2; border: none;"
|
||||
" padding: 4px 6px; text-align: left; }")
|
||||
.arg(surface, text));
|
||||
ui->processTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
ui->filterEdit->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
|
||||
"QLineEdit:focus { border-color: %4; }")
|
||||
.arg(bg, text, border, highlight));
|
||||
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
|
||||
"QPushButton:hover { background: %4; }"
|
||||
"QPushButton:pressed { background: %5; }"
|
||||
"QPushButton:disabled { color: %6; }")
|
||||
.arg(button, text, border, hover, hoverDk, mutedText);
|
||||
ui->refreshButton->setStyleSheet(btnStyle);
|
||||
ui->attachButton->setStyleSheet(btnStyle);
|
||||
ui->cancelButton->setStyleSheet(btnStyle);
|
||||
|
||||
// Right-click context menu
|
||||
ui->processTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(ui->processTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
int row = ui->processTable->rowAt(pos.y());
|
||||
if (row < 0) return;
|
||||
auto* pidItem = ui->processTable->item(row, 0);
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
auto* pathItem = ui->processTable->item(row, 2);
|
||||
if (!pidItem || !nameItem) return;
|
||||
|
||||
QString pid = QString::number(pidItem->data(Qt::EditRole).toUInt());
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
QString path = pathItem ? pathItem->text() : QString();
|
||||
|
||||
QMenu menu;
|
||||
auto* copyPid = menu.addAction(QStringLiteral("Copy PID"));
|
||||
auto* copyName = menu.addAction(QStringLiteral("Copy Name"));
|
||||
QAction* copyPath = nullptr;
|
||||
if (!path.isEmpty())
|
||||
copyPath = menu.addAction(QStringLiteral("Copy Path"));
|
||||
|
||||
auto* chosen = menu.exec(ui->processTable->viewport()->mapToGlobal(pos));
|
||||
if (chosen == copyPid)
|
||||
QApplication::clipboard()->setText(pid);
|
||||
else if (chosen == copyName)
|
||||
QApplication::clipboard()->setText(name);
|
||||
else if (copyPath && chosen == copyPath)
|
||||
QApplication::clipboard()->setText(path);
|
||||
});
|
||||
|
||||
// Auto-focus filter for immediate typing
|
||||
ui->filterEdit->setFocus();
|
||||
}
|
||||
|
||||
ProcessPicker::~ProcessPicker()
|
||||
@@ -97,31 +168,31 @@ void ProcessPicker::onProcessSelected()
|
||||
{
|
||||
auto* item = ui->processTable->currentItem();
|
||||
if (!item) return;
|
||||
|
||||
|
||||
int row = item->row();
|
||||
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
|
||||
// Use original name stored in UserRole (without architecture suffix)
|
||||
QVariant origName = ui->processTable->item(row, 1)->data(Qt::UserRole);
|
||||
m_selectedName = origName.isValid() ? origName.toString()
|
||||
: ui->processTable->item(row, 1)->text();
|
||||
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void ProcessPicker::enumerateProcesses()
|
||||
{
|
||||
QList<ProcessInfo> processes;
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||
QMessageBox::warning(this, "Error", "Failed to enumerate processes.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
PROCESSENTRY32W pe32;
|
||||
pe32.dwSize = sizeof(PROCESSENTRY32W);
|
||||
|
||||
|
||||
if (Process32FirstW(snapshot, &pe32))
|
||||
{
|
||||
do
|
||||
@@ -129,10 +200,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
ProcessInfo info;
|
||||
info.pid = pe32.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(pe32.szExeFile);
|
||||
|
||||
// Try to get full path and extract icon
|
||||
// If we can't open a process with PROCESS_QUERY_LIMITED_INFORMATION then
|
||||
// we for sure can't access their memory. - Skip in this case
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
|
||||
if (hProcess)
|
||||
{
|
||||
@@ -143,7 +211,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
GetModuleFileNameExW(hProcess, nullptr, path, pathLen))
|
||||
{
|
||||
info.path = QString::fromWCharArray(path);
|
||||
|
||||
|
||||
// Extract icon from executable
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
|
||||
@@ -262,6 +330,9 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
|
||||
pathItem->setToolTip(proc.path); // Show full path on hover
|
||||
ui->processTable->setItem(i, 2, pathItem);
|
||||
}
|
||||
|
||||
// Default sort: highest PID first (most recently launched processes on top)
|
||||
ui->processTable->sortItems(0, Qt::DescendingOrder);
|
||||
}
|
||||
|
||||
void ProcessPicker::filterProcesses(const QString& text)
|
||||
@@ -292,3 +363,22 @@ void ProcessPicker::applyFilter()
|
||||
|
||||
populateTable(filtered);
|
||||
}
|
||||
|
||||
void ProcessPicker::selectPreferredProcess()
|
||||
{
|
||||
// Try to select the last-attached process if it's in the list
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QString lastProc = s.value("lastAttachedProcess").toString();
|
||||
if (lastProc.isEmpty()) return;
|
||||
|
||||
for (int row = 0; row < ui->processTable->rowCount(); ++row) {
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
if (!nameItem) continue;
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
if (name.compare(lastProc, Qt::CaseInsensitive) == 0) {
|
||||
ui->processTable->selectRow(row);
|
||||
ui->processTable->scrollToItem(nameItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ private slots:
|
||||
void filterProcesses(const QString& text);
|
||||
|
||||
private:
|
||||
void initUi();
|
||||
void enumerateProcesses();
|
||||
void populateTable(const QList<ProcessInfo>& processes);
|
||||
void applyFilter();
|
||||
void selectPreferredProcess();
|
||||
|
||||
Ui::ProcessPicker *ui;
|
||||
uint32_t m_selectedPid = 0;
|
||||
|
||||
@@ -127,22 +127,6 @@
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>attachButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ProcessPicker</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>600</x>
|
||||
<y>470</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>350</x>
|
||||
<y>250</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>cancelButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "providerregistry.h"
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include <QIcon>
|
||||
#include <QHash>
|
||||
|
||||
ProviderRegistry& ProviderRegistry::instance() {
|
||||
static ProviderRegistry s_instance;
|
||||
@@ -56,3 +59,57 @@ const ProviderRegistry::ProviderInfo* ProviderRegistry::findProvider(const QStri
|
||||
void ProviderRegistry::clear() {
|
||||
m_providers.clear();
|
||||
}
|
||||
|
||||
void ProviderRegistry::populateSourceMenu(QMenu* menu,
|
||||
const QVector<SavedSourceDisplay>& savedSources)
|
||||
{
|
||||
static const QHash<QString, QString> s_providerIcons = {
|
||||
{QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")},
|
||||
{QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")},
|
||||
{QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")},
|
||||
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
|
||||
};
|
||||
|
||||
// File source
|
||||
auto* fileAct = menu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")),
|
||||
QStringLiteral("File"));
|
||||
fileAct->setIconVisibleInMenu(true);
|
||||
fileAct->setData(QStringLiteral("File"));
|
||||
|
||||
// Registered providers
|
||||
const auto& providers = instance().providers();
|
||||
for (const auto& prov : providers) {
|
||||
auto it = s_providerIcons.constFind(prov.identifier);
|
||||
QIcon icon(it != s_providerIcons.constEnd() ? *it
|
||||
: QStringLiteral(":/vsicons/extensions.svg"));
|
||||
|
||||
QString label = prov.dllFileName.isEmpty()
|
||||
? prov.name
|
||||
: QStringLiteral("%1 (%2)").arg(prov.name, prov.dllFileName);
|
||||
|
||||
auto* act = menu->addAction(icon, label);
|
||||
act->setIconVisibleInMenu(true);
|
||||
act->setData(prov.name); // routing key for selectSource()
|
||||
|
||||
// Plugin-specific actions (e.g. "Unload Driver" when loaded)
|
||||
if (prov.plugin)
|
||||
prov.plugin->populatePluginMenu(menu);
|
||||
}
|
||||
|
||||
// Saved sources
|
||||
if (!savedSources.isEmpty()) {
|
||||
menu->addSeparator();
|
||||
for (int i = 0; i < savedSources.size(); i++) {
|
||||
auto* act = menu->addAction(savedSources[i].text);
|
||||
act->setCheckable(true);
|
||||
act->setChecked(savedSources[i].active);
|
||||
act->setData(QStringLiteral("#saved:%1").arg(i));
|
||||
}
|
||||
menu->addSeparator();
|
||||
auto* clearAct = menu->addAction(
|
||||
QIcon(QStringLiteral(":/vsicons/clear-all.svg")),
|
||||
QStringLiteral("Clear All"));
|
||||
clearAct->setIconVisibleInMenu(true);
|
||||
clearAct->setData(QStringLiteral("#clear"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
// Forward declarations
|
||||
namespace rcx { class Provider; }
|
||||
class QWidget;
|
||||
class QMenu;
|
||||
|
||||
// Lightweight struct for saved source display in menus
|
||||
struct SavedSourceDisplay {
|
||||
QString text;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global registry for data source providers
|
||||
@@ -56,7 +63,13 @@ public:
|
||||
|
||||
// Clear all providers
|
||||
void clear();
|
||||
|
||||
|
||||
// Populate a QMenu with source items (File, providers with icons/dll names,
|
||||
// plugin actions, saved sources). Used by both the main window Data Source
|
||||
// menu and the RcxEditor inline source picker.
|
||||
static void populateSourceMenu(QMenu* menu,
|
||||
const QVector<SavedSourceDisplay>& savedSources = {});
|
||||
|
||||
private:
|
||||
ProviderRegistry() = default;
|
||||
QList<ProviderInfo> m_providers;
|
||||
|
||||
@@ -16,6 +16,13 @@ struct MemoryRegion {
|
||||
QString moduleName;
|
||||
};
|
||||
|
||||
struct VtopResult {
|
||||
uint64_t physical = 0;
|
||||
uint64_t pml4e = 0, pdpte = 0, pde = 0, pte = 0;
|
||||
uint8_t pageSize = 0; // 0=4KB, 1=2MB, 2=1GB
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
class Provider {
|
||||
public:
|
||||
virtual ~Provider() = default;
|
||||
@@ -73,6 +80,29 @@ public:
|
||||
// Default: returns empty (scan engine falls back to [0, size())).
|
||||
virtual QVector<MemoryRegion> enumerateRegions() const { return {}; }
|
||||
|
||||
// Process Environment Block address (x64 PEB VA in target process).
|
||||
// Only meaningful for live process providers. Returns 0 if unavailable.
|
||||
virtual uint64_t peb() const { return 0; }
|
||||
|
||||
struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; };
|
||||
virtual QVector<ThreadInfo> tebs() const { return {}; }
|
||||
|
||||
struct ModuleEntry { QString name; QString fullPath; uint64_t base; uint64_t size; };
|
||||
virtual QVector<ModuleEntry> enumerateModules() const { return {}; }
|
||||
|
||||
// --- Kernel paging capabilities (override in kernel providers) ---
|
||||
virtual bool hasKernelPaging() const { return false; }
|
||||
virtual uint64_t getCr3() const { return 0; }
|
||||
virtual VtopResult translateAddress(uint64_t va) const {
|
||||
Q_UNUSED(va); return {};
|
||||
}
|
||||
virtual QVector<uint64_t> readPageTable(uint64_t physAddr,
|
||||
int startIdx = 0,
|
||||
int count = 512) const {
|
||||
Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count);
|
||||
return {};
|
||||
}
|
||||
|
||||
// --- Derived convenience (non-virtual, never override) ---
|
||||
|
||||
bool isValid() const { return size() > 0; }
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
bool isReadable(uint64_t addr, int len) const override {
|
||||
if (len <= 0) return (len == 0);
|
||||
uint64_t end = addr + static_cast<uint64_t>(len);
|
||||
if (end < addr) return false; // overflow
|
||||
for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) {
|
||||
if (!m_pages.contains(p)) return false;
|
||||
}
|
||||
|
||||
173
src/rcxtooltip.h
Normal file
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
#include <QWidget>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QScreen>
|
||||
#include <QApplication>
|
||||
#include <QMouseEvent>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Modern arrow tooltip ──
|
||||
// Draws a rounded-rect body with a triangular arrow whose tip touches
|
||||
// the anchor point (center of the dwell area).
|
||||
//
|
||||
// Bypasses Fusion/CSS/DWM entirely — everything is manual QPainter on a
|
||||
// WA_TranslucentBackground layered window. The DarkTitleBar property is
|
||||
// pre-set to prevent DarkApp::notify from calling DwmSetWindowAttribute
|
||||
// (which was the root cause of the previous transparent-window failure).
|
||||
//
|
||||
// Usage:
|
||||
// tip->setTheme(bg, border, titleCol, bodyCol, sepCol);
|
||||
// tip->populate("Title", "line1\nline2", font);
|
||||
// tip->showAt(QPoint(midX, lineBottom)); // arrow tip at this point
|
||||
// tip->dismiss();
|
||||
|
||||
class RcxTooltip : public QWidget {
|
||||
public:
|
||||
static constexpr int kArrowH = 8;
|
||||
static constexpr int kArrowW = 14;
|
||||
static constexpr int kRadius = 6;
|
||||
static constexpr int kPad = 10;
|
||||
static constexpr int kGap = 4;
|
||||
static constexpr int kMaxW = 550;
|
||||
|
||||
std::function<void(QMouseEvent*)> onMouseMove;
|
||||
|
||||
explicit RcxTooltip(QWidget* parent = nullptr)
|
||||
: QWidget(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
// ── Key fix: prevent DwmSetWindowAttribute on this window ──
|
||||
// DarkApp::notify checks this property and skips DWM calls.
|
||||
// Without this, DWMWA_USE_IMMERSIVE_DARK_MODE breaks WS_EX_LAYERED
|
||||
// alpha compositing on Windows 10/11.
|
||||
setProperty("DarkTitleBar", true);
|
||||
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void setTheme(const QColor& bg, const QColor& border,
|
||||
const QColor& title, const QColor& body, const QColor& sep) {
|
||||
m_bg = bg; m_border = border;
|
||||
m_titleCol = title; m_bodyCol = body; m_sepCol = sep;
|
||||
}
|
||||
|
||||
void populate(const QString& title, const QString& body, const QFont& font) {
|
||||
if (title == m_title && body == m_body && isVisible()) return;
|
||||
m_title = title; m_body = body;
|
||||
m_lines = body.split('\n');
|
||||
m_font = font;
|
||||
m_font.setPointSizeF(font.pointSizeF() * 0.9);
|
||||
m_bold = m_font; m_bold.setBold(true);
|
||||
recalc();
|
||||
}
|
||||
|
||||
// `anchor`: global screen point where the arrow tip touches.
|
||||
// Typically the center-bottom of the hovered span.
|
||||
void showAt(const QPoint& anchor) {
|
||||
QRect scr = screenAt(anchor);
|
||||
int w = m_bw, h = m_bh + kArrowH;
|
||||
m_up = (anchor.y() + h <= scr.bottom());
|
||||
int x = qBound(scr.left() + 2, anchor.x() - w / 2, scr.right() - w - 2);
|
||||
int y = m_up ? anchor.y() : anchor.y() - h;
|
||||
m_ax = qBound(kRadius + kArrowW/2 + 1, anchor.x() - x,
|
||||
w - kRadius - kArrowW/2 - 1);
|
||||
setFixedSize(w, h);
|
||||
move(x, y);
|
||||
if (!isVisible()) show();
|
||||
update();
|
||||
}
|
||||
|
||||
void dismiss() { if (isVisible()) hide(); }
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Body rect (excludes arrow space)
|
||||
QRectF b(0.5, m_up ? kArrowH + 0.5 : 0.5,
|
||||
width() - 1.0, m_bh - 1.0);
|
||||
qreal r = kRadius, ax = m_ax, ah = kArrowW / 2.0;
|
||||
|
||||
// ── Single contiguous path: rounded rect + arrow notch ──
|
||||
// No QPainterPath::united() — that causes junction artifacts.
|
||||
// Clockwise from top-left, inserting the arrow inline.
|
||||
QPainterPath pp;
|
||||
pp.moveTo(b.left() + r, b.top());
|
||||
if (m_up) {
|
||||
pp.lineTo(ax - ah, b.top());
|
||||
pp.lineTo(ax, 0.5);
|
||||
pp.lineTo(ax + ah, b.top());
|
||||
}
|
||||
pp.lineTo(b.right() - r, b.top());
|
||||
pp.arcTo(b.right() - 2*r, b.top(), 2*r, 2*r, 90, -90);
|
||||
pp.lineTo(b.right(), b.bottom() - r);
|
||||
pp.arcTo(b.right() - 2*r, b.bottom() - 2*r, 2*r, 2*r, 0, -90);
|
||||
if (!m_up) {
|
||||
pp.lineTo(ax + ah, b.bottom());
|
||||
pp.lineTo(ax, height() - 0.5);
|
||||
pp.lineTo(ax - ah, b.bottom());
|
||||
}
|
||||
pp.lineTo(b.left() + r, b.bottom());
|
||||
pp.arcTo(b.left(), b.bottom() - 2*r, 2*r, 2*r, 270, -90);
|
||||
pp.lineTo(b.left(), b.top() + r);
|
||||
pp.arcTo(b.left(), b.top(), 2*r, 2*r, 180, -90);
|
||||
pp.closeSubpath();
|
||||
|
||||
p.setPen(QPen(m_border, 1));
|
||||
p.setBrush(m_bg);
|
||||
p.drawPath(pp);
|
||||
|
||||
// ── Content: title + separator + body ──
|
||||
qreal cy = (m_up ? kArrowH : 0) + kPad;
|
||||
QFontMetrics tf(m_bold), bf(m_font);
|
||||
|
||||
if (!m_title.isEmpty()) {
|
||||
p.setFont(m_bold); p.setPen(m_titleCol);
|
||||
p.drawText(QPointF(kPad, cy + tf.ascent()), m_title);
|
||||
cy += tf.height() + kGap;
|
||||
p.setPen(m_sepCol);
|
||||
p.drawLine(QPointF(kPad, cy), QPointF(width() - kPad, cy));
|
||||
cy += 1 + kGap;
|
||||
}
|
||||
p.setFont(m_font); p.setPen(m_bodyCol);
|
||||
for (const auto& l : m_lines) {
|
||||
p.drawText(QPointF(kPad, cy + bf.ascent()), l);
|
||||
cy += bf.lineSpacing();
|
||||
}
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
if (onMouseMove) onMouseMove(e); else QWidget::mouseMoveEvent(e);
|
||||
}
|
||||
|
||||
private:
|
||||
static QRect screenAt(const QPoint& pt) {
|
||||
auto* s = QApplication::screenAt(pt);
|
||||
return s ? s->availableGeometry() : QRect(0, 0, 1920, 1080);
|
||||
}
|
||||
|
||||
void recalc() {
|
||||
QFontMetrics tf(m_bold), bf(m_font);
|
||||
int maxW = m_title.isEmpty() ? 0 : tf.horizontalAdvance(m_title);
|
||||
for (const auto& l : m_lines) maxW = qMax(maxW, bf.horizontalAdvance(l));
|
||||
m_bw = qMin(maxW + 2 * kPad, kMaxW);
|
||||
m_bh = kPad + (m_title.isEmpty() ? 0 : tf.height() + kGap + 1 + kGap)
|
||||
+ m_lines.size() * bf.lineSpacing() + kPad;
|
||||
}
|
||||
|
||||
QString m_title, m_body;
|
||||
QStringList m_lines;
|
||||
QFont m_font, m_bold;
|
||||
QColor m_bg{30, 30, 30}, m_border{60, 60, 60};
|
||||
QColor m_titleCol{220, 220, 220}, m_bodyCol{180, 180, 180}, m_sepCol{60, 60, 60};
|
||||
bool m_up = true;
|
||||
int m_ax = 0, m_bw = 0, m_bh = 0;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
@@ -17,6 +17,7 @@
|
||||
<file alias="file-binary.svg">vsicons/file-binary.svg</file>
|
||||
<file alias="debug.svg">vsicons/debug.svg</file>
|
||||
<file alias="close.svg">vsicons/close.svg</file>
|
||||
<file alias="cloud-download.svg">vsicons/cloud-download.svg</file>
|
||||
<file alias="arrow-left.svg">vsicons/arrow-left.svg</file>
|
||||
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
|
||||
<file alias="split-horizontal.svg">vsicons/split-horizontal.svg</file>
|
||||
@@ -49,10 +50,13 @@
|
||||
<file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file>
|
||||
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
|
||||
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
|
||||
<file alias="chevron-right.svg">vsicons/chevron-right.svg</file>
|
||||
<file alias="chevron-left.svg">vsicons/chevron-left.svg</file>
|
||||
<file alias="folder.svg">vsicons/folder.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-variable.svg">vsicons/symbol-variable.svg</file>
|
||||
<file alias="symbol-method.svg">vsicons/symbol-method.svg</file>
|
||||
<file alias="server-process.svg">vsicons/server-process.svg</file>
|
||||
<file alias="remote.svg">vsicons/remote.svg</file>
|
||||
<file alias="plug.svg">vsicons/plug.svg</file>
|
||||
@@ -60,5 +64,10 @@
|
||||
<file alias="search.svg">vsicons/search.svg</file>
|
||||
<file alias="regex.svg">vsicons/regex.svg</file>
|
||||
<file alias="refresh.svg">vsicons/refresh.svg</file>
|
||||
<file alias="pin.svg">vsicons/pin.svg</file>
|
||||
<file alias="pinned.svg">vsicons/pinned.svg</file>
|
||||
<file alias="close-all.svg">vsicons/close-all.svg</file>
|
||||
<file alias="split-vertical.svg">vsicons/split-vertical.svg</file>
|
||||
<file alias="book.svg">vsicons/book.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
251
src/scanner.cpp
@@ -347,6 +347,64 @@ int naturalAlignment(ValueType type) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int valueSizeForType(ValueType type) {
|
||||
switch (type) {
|
||||
case ValueType::Int8: case ValueType::UInt8: return 1;
|
||||
case ValueType::Int16: case ValueType::UInt16: return 2;
|
||||
case ValueType::Int32: case ValueType::UInt32: case ValueType::Float: return 4;
|
||||
case ValueType::Int64: case ValueType::UInt64: case ValueType::Double: return 8;
|
||||
case ValueType::Vec2: return 8;
|
||||
case ValueType::Vec3: return 12;
|
||||
case ValueType::Vec4: return 16;
|
||||
default: return 4;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Typed comparison for rescan conditions ──
|
||||
|
||||
static int compareTyped(const QByteArray& a, const QByteArray& b, ValueType vt) {
|
||||
const char* da = a.constData();
|
||||
const char* db = b.constData();
|
||||
int sz = qMin(a.size(), b.size());
|
||||
|
||||
switch (vt) {
|
||||
case ValueType::Int8:
|
||||
if (sz >= 1) { int8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::UInt8:
|
||||
if (sz >= 1) { uint8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::Int16:
|
||||
if (sz >= 2) { int16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::UInt16:
|
||||
if (sz >= 2) { uint16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::Int32:
|
||||
if (sz >= 4) { int32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::UInt32:
|
||||
if (sz >= 4) { uint32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::Int64:
|
||||
if (sz >= 8) { int64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::UInt64:
|
||||
if (sz >= 8) { uint64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::Float:
|
||||
if (sz >= 4) { float va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
case ValueType::Double:
|
||||
if (sz >= 8) { double va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Fallback: byte comparison
|
||||
return memcmp(da, db, sz);
|
||||
}
|
||||
|
||||
// ── Scan engine ──
|
||||
|
||||
ScanEngine::ScanEngine(QObject* parent)
|
||||
@@ -366,13 +424,15 @@ void ScanEngine::abort() {
|
||||
void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& req) {
|
||||
if (isRunning()) return;
|
||||
|
||||
if (req.pattern.isEmpty()) {
|
||||
emit error(QStringLiteral("Empty pattern"));
|
||||
return;
|
||||
}
|
||||
if (req.pattern.size() != req.mask.size()) {
|
||||
emit error(QStringLiteral("Pattern and mask size mismatch"));
|
||||
return;
|
||||
if (req.condition != ScanCondition::UnknownValue) {
|
||||
if (req.pattern.isEmpty()) {
|
||||
emit error(QStringLiteral("Empty pattern"));
|
||||
return;
|
||||
}
|
||||
if (req.pattern.size() != req.mask.size()) {
|
||||
emit error(QStringLiteral("Pattern and mask size mismatch"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_abort.store(false);
|
||||
@@ -400,39 +460,85 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
timer.start();
|
||||
|
||||
QVector<ScanResult> results;
|
||||
const bool isUnknown = (req.condition == ScanCondition::UnknownValue);
|
||||
|
||||
if (!prov || req.pattern.isEmpty())
|
||||
if (!prov || (!isUnknown && req.pattern.isEmpty()))
|
||||
return results;
|
||||
|
||||
auto regions = prov->enumerateRegions();
|
||||
qDebug() << "[scan] regions:" << regions.size()
|
||||
<< " pattern:" << req.pattern.size() << "bytes"
|
||||
<< " align:" << req.alignment
|
||||
<< " condition:" << (int)req.condition
|
||||
<< " filterExec:" << req.filterExecutable
|
||||
<< " filterWrite:" << req.filterWritable;
|
||||
|
||||
// Fallback for providers that don't enumerate regions (file/buffer)
|
||||
// Fallback for providers that don't enumerate regions (file/buffer/syscall without modules)
|
||||
if (regions.isEmpty()) {
|
||||
MemoryRegion fallback;
|
||||
fallback.base = 0;
|
||||
fallback.size = (uint64_t)prov->size();
|
||||
fallback.readable = true;
|
||||
fallback.writable = true;
|
||||
fallback.executable = false;
|
||||
fallback.executable = true; // unknown; include so filters don't exclude the only region
|
||||
regions.append(fallback);
|
||||
}
|
||||
|
||||
const int patternLen = req.pattern.size();
|
||||
const char* pat = req.pattern.constData();
|
||||
const char* msk = req.mask.constData();
|
||||
const int patternLen = isUnknown ? req.valueSize : req.pattern.size();
|
||||
const char* pat = isUnknown ? nullptr : req.pattern.constData();
|
||||
const char* msk = isUnknown ? nullptr : req.mask.constData();
|
||||
const int alignment = qMax(1, req.alignment);
|
||||
const int valSize = isUnknown ? req.valueSize : patternLen;
|
||||
const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) &&
|
||||
req.endAddress > req.startAddress;
|
||||
|
||||
// If constrainRegions specified, intersect with provider regions
|
||||
if (!req.constrainRegions.isEmpty()) {
|
||||
// Sort and merge overlapping/adjacent constraints to avoid duplicate sub-regions
|
||||
auto constraints = req.constrainRegions;
|
||||
std::sort(constraints.begin(), constraints.end(),
|
||||
[](const AddressRange& a, const AddressRange& b) { return a.start < b.start; });
|
||||
QVector<AddressRange> merged;
|
||||
for (const auto& c : constraints) {
|
||||
if (c.end <= c.start) continue; // skip degenerate ranges
|
||||
if (!merged.isEmpty() && c.start <= merged.last().end)
|
||||
merged.last().end = qMax(merged.last().end, c.end);
|
||||
else
|
||||
merged.append(c);
|
||||
}
|
||||
|
||||
QVector<MemoryRegion> clipped;
|
||||
for (const auto& region : regions) {
|
||||
uint64_t rEnd = region.base + region.size;
|
||||
for (const auto& c : merged) {
|
||||
if (c.end <= region.base || c.start >= rEnd) continue;
|
||||
uint64_t iStart = qMax(region.base, c.start);
|
||||
uint64_t iEnd = qMin(rEnd, c.end);
|
||||
if (iEnd <= iStart) continue;
|
||||
MemoryRegion sub = region;
|
||||
sub.base = iStart;
|
||||
sub.size = iEnd - iStart;
|
||||
clipped.append(sub);
|
||||
}
|
||||
}
|
||||
regions = std::move(clipped);
|
||||
qDebug() << "[scan] constrained to" << regions.size() << "sub-regions from"
|
||||
<< req.constrainRegions.size() << "address ranges ("
|
||||
<< merged.size() << "after merge)";
|
||||
}
|
||||
|
||||
// Pre-compute total bytes for progress
|
||||
uint64_t totalBytes = 0;
|
||||
for (const auto& r : regions) {
|
||||
if (req.filterExecutable && !r.executable) continue;
|
||||
if (req.filterWritable && !r.writable) continue;
|
||||
totalBytes += r.size;
|
||||
uint64_t rStart = r.base, rEnd = r.base + r.size;
|
||||
if (hasRange) {
|
||||
if (rEnd <= req.startAddress || rStart >= req.endAddress) continue;
|
||||
rStart = qMax(rStart, req.startAddress);
|
||||
rEnd = qMin(rEnd, req.endAddress);
|
||||
}
|
||||
totalBytes += rEnd - rStart;
|
||||
}
|
||||
|
||||
qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";
|
||||
@@ -444,28 +550,46 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
|
||||
constexpr int kChunk = 256 * 1024;
|
||||
|
||||
for (const auto& region : regions) {
|
||||
for (int regionIndex = 0; regionIndex < regions.size(); ++regionIndex) {
|
||||
const auto& region = regions[regionIndex];
|
||||
if (m_abort.load()) break;
|
||||
|
||||
if (req.filterExecutable && !region.executable) continue;
|
||||
if (req.filterWritable && !region.writable) continue;
|
||||
|
||||
if ((uint64_t)patternLen > region.size) {
|
||||
scannedBytes += region.size;
|
||||
// Clip region to requested address range
|
||||
uint64_t regStart = region.base;
|
||||
uint64_t regEnd = region.base + region.size;
|
||||
if (hasRange) {
|
||||
if (regEnd <= req.startAddress || regStart >= req.endAddress) {
|
||||
// Entirely outside range — skip
|
||||
continue;
|
||||
}
|
||||
regStart = qMax(regStart, req.startAddress);
|
||||
regEnd = qMin(regEnd, req.endAddress);
|
||||
}
|
||||
uint64_t regSize = regEnd - regStart;
|
||||
if (regSize == 0) continue;
|
||||
|
||||
if ((uint64_t)patternLen > regSize) {
|
||||
scannedBytes += regSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int overlap = patternLen - 1;
|
||||
QByteArray chunk(qMin((uint64_t)kChunk, region.size), Qt::Uninitialized);
|
||||
QByteArray chunk(qMin((uint64_t)kChunk, regSize), Qt::Uninitialized);
|
||||
uint64_t regOffset = regStart - region.base; // offset within provider region
|
||||
|
||||
for (uint64_t off = 0; off < region.size; ) {
|
||||
for (uint64_t off = 0; off < regSize; ) {
|
||||
if (m_abort.load()) break;
|
||||
|
||||
uint64_t remaining = region.size - off;
|
||||
uint64_t remaining = regSize - off;
|
||||
int readLen = (int)qMin((uint64_t)chunk.size(), remaining);
|
||||
|
||||
if (!prov->read(region.base + off, chunk.data(), readLen)) {
|
||||
if (!prov->read(regStart + off, chunk.data(), readLen)) {
|
||||
// Skip unreadable chunk
|
||||
qDebug() << "[scan] read failed region" << regionIndex << "addr" << Qt::showbase << Qt::hex
|
||||
<< (region.base + off) << "base" << region.base << "off" << off << "len" << readLen << Qt::dec;
|
||||
off += readLen;
|
||||
scannedBytes += readLen;
|
||||
continue;
|
||||
@@ -474,29 +598,46 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
int scanEnd = readLen - patternLen;
|
||||
const char* data = chunk.constData();
|
||||
|
||||
for (int i = 0; i <= scanEnd; i += alignment) {
|
||||
bool match = true;
|
||||
for (int j = 0; j < patternLen; j++) {
|
||||
if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
if (isUnknown) {
|
||||
// Unknown value: capture every aligned address
|
||||
for (int i = 0; i <= scanEnd; i += alignment) {
|
||||
ScanResult r;
|
||||
r.address = region.base + off + (uint64_t)i;
|
||||
r.regionModule = region.moduleName;
|
||||
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
|
||||
r.address = regStart + off + (uint64_t)i;
|
||||
r.scanValue = QByteArray(data + i, valSize);
|
||||
results.append(r);
|
||||
|
||||
if (results.size() >= req.maxResults)
|
||||
goto done;
|
||||
}
|
||||
} else {
|
||||
// Exact pattern match
|
||||
for (int i = 0; i <= scanEnd; i += alignment) {
|
||||
bool match = true;
|
||||
for (int j = 0; j < patternLen; j++) {
|
||||
if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
ScanResult r;
|
||||
r.address = regStart + off + (uint64_t)i;
|
||||
r.regionModule = region.moduleName;
|
||||
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
|
||||
results.append(r);
|
||||
|
||||
if (results.size() >= req.maxResults)
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance with overlap to catch patterns that straddle chunks
|
||||
// Advance with overlap to catch patterns that straddle chunks.
|
||||
// Skip overlap on the final chunk -- nothing follows to overlap into.
|
||||
uint64_t advance;
|
||||
if (readLen > overlap)
|
||||
if ((uint64_t)readLen >= remaining)
|
||||
advance = remaining; // last chunk, no overlap needed
|
||||
else if (readLen > overlap)
|
||||
advance = (uint64_t)(readLen - overlap);
|
||||
else
|
||||
advance = 1; // prevent infinite loop on tiny regions
|
||||
@@ -522,6 +663,7 @@ done:
|
||||
|
||||
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
ScanCondition condition, ValueType valueType,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask) {
|
||||
if (isRunning()) return;
|
||||
@@ -541,14 +683,15 @@ void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
||||
|
||||
watcher->setFuture(QtConcurrent::run(
|
||||
[this, provider, results = std::move(results), readSize,
|
||||
filterPattern, filterMask]() mutable {
|
||||
condition, valueType, filterPattern, filterMask]() mutable {
|
||||
return runRescan(provider, std::move(results), readSize,
|
||||
filterPattern, filterMask);
|
||||
condition, valueType, filterPattern, filterMask);
|
||||
}));
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
ScanCondition condition, ValueType valueType,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask) {
|
||||
QElapsedTimer timer;
|
||||
@@ -557,9 +700,17 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
int total = results.size();
|
||||
if (total == 0 || !prov) return results;
|
||||
|
||||
bool hasFilter = !filterPattern.isEmpty();
|
||||
bool hasExactFilter = !filterPattern.isEmpty() && condition == ScanCondition::ExactValue;
|
||||
bool hasComparison = (condition == ScanCondition::Changed ||
|
||||
condition == ScanCondition::Unchanged ||
|
||||
condition == ScanCondition::Increased ||
|
||||
condition == ScanCondition::Decreased);
|
||||
bool needsFilter = hasExactFilter || hasComparison;
|
||||
|
||||
qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize
|
||||
<< "filter:" << (hasFilter ? "yes" : "no");
|
||||
<< "condition:" << (int)condition
|
||||
<< "exactFilter:" << (hasExactFilter ? "yes" : "no")
|
||||
<< "comparison:" << (hasComparison ? "yes" : "no");
|
||||
|
||||
// Save previous values
|
||||
for (auto& r : results)
|
||||
@@ -579,8 +730,8 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
uint64_t totalBytesRead = 0;
|
||||
int i = 0;
|
||||
|
||||
// Track which results matched the filter (by original index)
|
||||
QVector<bool> matched(total, !hasFilter); // if no filter, all match
|
||||
// Track which results matched (by original index)
|
||||
QVector<bool> matched(total, !needsFilter); // if no filter, all match
|
||||
|
||||
while (i < total && !m_abort.load()) {
|
||||
uint64_t spanBase = results[order[i]].address;
|
||||
@@ -604,8 +755,8 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
int off = (int)(r.address - spanBase);
|
||||
r.scanValue = chunk.mid(off, readSize);
|
||||
|
||||
// Apply filter: compare re-read bytes against the new pattern
|
||||
if (hasFilter) {
|
||||
// Apply exact-value filter
|
||||
if (hasExactFilter) {
|
||||
int patLen = filterPattern.size();
|
||||
if (r.scanValue.size() >= patLen) {
|
||||
bool ok = true;
|
||||
@@ -621,6 +772,18 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
matched[idx] = ok;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply comparison-based filter
|
||||
if (hasComparison && !r.previousValue.isEmpty()) {
|
||||
int cmp = compareTyped(r.scanValue, r.previousValue, valueType);
|
||||
switch (condition) {
|
||||
case ScanCondition::Changed: matched[idx] = (cmp != 0); break;
|
||||
case ScanCondition::Unchanged: matched[idx] = (cmp == 0); break;
|
||||
case ScanCondition::Increased: matched[idx] = (cmp > 0); break;
|
||||
case ScanCondition::Decreased: matched[idx] = (cmp < 0); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chunks++;
|
||||
@@ -637,7 +800,7 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
}
|
||||
|
||||
// Filter out non-matching results
|
||||
if (hasFilter) {
|
||||
if (needsFilter) {
|
||||
QVector<ScanResult> filtered;
|
||||
filtered.reserve(total);
|
||||
for (int k = 0; k < total; k++) {
|
||||
|
||||
@@ -10,26 +10,6 @@
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Scan request / result ──
|
||||
|
||||
struct ScanRequest {
|
||||
QByteArray pattern; // literal bytes to match
|
||||
QByteArray mask; // 0xFF = must match, 0x00 = wildcard
|
||||
|
||||
bool filterExecutable = false; // only scan +x regions
|
||||
bool filterWritable = false; // only scan +w regions
|
||||
|
||||
int alignment = 1; // 1 = every byte, 4 = dword, 8 = qword
|
||||
int maxResults = 50000;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
uint64_t address;
|
||||
QString regionModule;
|
||||
QByteArray scanValue; // cached bytes at scan/update time
|
||||
QByteArray previousValue; // value before last update
|
||||
};
|
||||
|
||||
// ── Value scan types ──
|
||||
|
||||
enum class ValueType {
|
||||
@@ -41,6 +21,51 @@ enum class ValueType {
|
||||
HexBytes
|
||||
};
|
||||
|
||||
// ── Scan condition (Cheat Engine-style) ──
|
||||
|
||||
enum class ScanCondition {
|
||||
ExactValue, // first scan + rescan: match specific bytes
|
||||
UnknownValue, // first scan only: capture all aligned addresses
|
||||
Changed, // rescan: current != previous
|
||||
Unchanged, // rescan: current == previous
|
||||
Increased, // rescan: current > previous (numeric)
|
||||
Decreased // rescan: current < previous (numeric)
|
||||
};
|
||||
|
||||
// ── Scan request / result ──
|
||||
|
||||
struct AddressRange {
|
||||
uint64_t start = 0;
|
||||
uint64_t end = 0; // exclusive
|
||||
};
|
||||
|
||||
struct ScanRequest {
|
||||
QByteArray pattern; // literal bytes to match (empty for UnknownValue)
|
||||
QByteArray mask; // 0xFF = must match, 0x00 = wildcard
|
||||
|
||||
bool filterExecutable = false; // only scan +x regions
|
||||
bool filterWritable = false; // only scan +w regions
|
||||
|
||||
int alignment = 1; // 1 = every byte, 4 = dword, 8 = qword
|
||||
int maxResults = 50000;
|
||||
|
||||
ScanCondition condition = ScanCondition::ExactValue;
|
||||
int valueSize = 4; // bytes per value (for unknown scans)
|
||||
|
||||
uint64_t startAddress = 0; // 0 = no limit (scan all regions)
|
||||
uint64_t endAddress = 0; // 0 = no limit (scan all regions)
|
||||
|
||||
// If non-empty, only scan within these address ranges (intersected with provider regions).
|
||||
QVector<AddressRange> constrainRegions;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
uint64_t address;
|
||||
QString regionModule;
|
||||
QByteArray scanValue; // cached bytes at scan/update time
|
||||
QByteArray previousValue; // value before last update
|
||||
};
|
||||
|
||||
// ── Pattern parsing ──
|
||||
|
||||
// Parse IDA-style signature string ("48 8B ?? 05") into pattern + mask.
|
||||
@@ -57,6 +82,9 @@ bool serializeValue(ValueType type, const QString& input,
|
||||
// Natural alignment for a value type (used as default alignment for value scans).
|
||||
int naturalAlignment(ValueType type);
|
||||
|
||||
// Byte-size for a value type (used for unknown scans and rescan read size).
|
||||
int valueSizeForType(ValueType type);
|
||||
|
||||
// ── Scan engine ──
|
||||
|
||||
class ScanEngine : public QObject {
|
||||
@@ -67,6 +95,8 @@ public:
|
||||
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
||||
void startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
ScanCondition condition = ScanCondition::ExactValue,
|
||||
ValueType valueType = ValueType::Int32,
|
||||
const QByteArray& filterPattern = {},
|
||||
const QByteArray& filterMask = {});
|
||||
void abort();
|
||||
@@ -82,6 +112,7 @@ private:
|
||||
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
||||
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
ScanCondition condition, ValueType valueType,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QEventLoop>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -93,6 +94,18 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
m_typeCombo->setCurrentIndex(2); // default: int32
|
||||
inputRow->addWidget(m_typeCombo);
|
||||
|
||||
m_condLabel = new QLabel(QStringLiteral("Scan:"), this);
|
||||
inputRow->addWidget(m_condLabel);
|
||||
|
||||
m_condCombo = new QComboBox(this);
|
||||
m_condCombo->addItem(QStringLiteral("Exact Value"), (int)ScanCondition::ExactValue);
|
||||
m_condCombo->addItem(QStringLiteral("Unknown Value"), (int)ScanCondition::UnknownValue);
|
||||
m_condCombo->addItem(QStringLiteral("Changed"), (int)ScanCondition::Changed);
|
||||
m_condCombo->addItem(QStringLiteral("Unchanged"), (int)ScanCondition::Unchanged);
|
||||
m_condCombo->addItem(QStringLiteral("Increased"), (int)ScanCondition::Increased);
|
||||
m_condCombo->addItem(QStringLiteral("Decreased"), (int)ScanCondition::Decreased);
|
||||
inputRow->addWidget(m_condCombo);
|
||||
|
||||
m_valueLabel = new QLabel(QStringLiteral("Value:"), this);
|
||||
inputRow->addWidget(m_valueLabel);
|
||||
|
||||
@@ -112,6 +125,9 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
m_writeCheck = new QCheckBox(QStringLiteral("Writable"), this);
|
||||
filterRow->addWidget(m_writeCheck);
|
||||
|
||||
m_structOnlyCheck = new QCheckBox(QStringLiteral("Current Struct"), this);
|
||||
filterRow->addWidget(m_structOnlyCheck);
|
||||
|
||||
filterRow->addStretch();
|
||||
|
||||
m_scanBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/search.svg")),
|
||||
@@ -168,12 +184,15 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
QStringLiteral("Copy Address"), this);
|
||||
m_copyBtn->setEnabled(false);
|
||||
actionRow->addWidget(m_copyBtn);
|
||||
actionRow->addSpacing(20); // room for resize grip when floating
|
||||
|
||||
mainLayout->addLayout(actionRow);
|
||||
|
||||
// ── Initial state: signature mode ──
|
||||
m_typeLabel->hide();
|
||||
m_typeCombo->hide();
|
||||
m_condLabel->hide();
|
||||
m_condCombo->hide();
|
||||
m_valueLabel->hide();
|
||||
m_valueEdit->hide();
|
||||
m_execCheck->setChecked(true);
|
||||
@@ -181,6 +200,8 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
// ── Connections ──
|
||||
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ScannerPanel::onModeChanged);
|
||||
connect(m_condCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ScannerPanel::onConditionChanged);
|
||||
connect(m_scanBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onScanClicked);
|
||||
connect(m_updateBtn, &QPushButton::clicked,
|
||||
@@ -241,6 +262,10 @@ void ScannerPanel::setProviderGetter(ProviderGetter getter) {
|
||||
m_providerGetter = std::move(getter);
|
||||
}
|
||||
|
||||
void ScannerPanel::setBoundsGetter(BoundsGetter getter) {
|
||||
m_boundsGetter = std::move(getter);
|
||||
}
|
||||
|
||||
void ScannerPanel::setEditorFont(const QFont& font) {
|
||||
m_resultTable->setFont(font);
|
||||
QFontMetrics fm(font);
|
||||
@@ -251,15 +276,18 @@ void ScannerPanel::setEditorFont(const QFont& font) {
|
||||
m_valueEdit->setFont(font);
|
||||
m_modeCombo->setFont(font);
|
||||
m_typeCombo->setFont(font);
|
||||
m_condCombo->setFont(font);
|
||||
m_statusLabel->setFont(font);
|
||||
m_scanBtn->setFont(font);
|
||||
m_gotoBtn->setFont(font);
|
||||
m_copyBtn->setFont(font);
|
||||
m_patternLabel->setFont(font);
|
||||
m_typeLabel->setFont(font);
|
||||
m_condLabel->setFont(font);
|
||||
m_valueLabel->setFont(font);
|
||||
m_execCheck->setFont(font);
|
||||
m_writeCheck->setFont(font);
|
||||
m_structOnlyCheck->setFont(font);
|
||||
m_updateBtn->setFont(font);
|
||||
updateComboWidth();
|
||||
}
|
||||
@@ -280,14 +308,29 @@ void ScannerPanel::onModeChanged(int index) {
|
||||
|
||||
m_typeLabel->setVisible(!isSig);
|
||||
m_typeCombo->setVisible(!isSig);
|
||||
m_condLabel->setVisible(!isSig);
|
||||
m_condCombo->setVisible(!isSig);
|
||||
|
||||
// Enable/disable value input based on condition
|
||||
auto cond = (ScanCondition)m_condCombo->currentData().toInt();
|
||||
bool needsValue = !isSig && (cond == ScanCondition::ExactValue);
|
||||
m_valueLabel->setVisible(!isSig);
|
||||
m_valueEdit->setVisible(!isSig);
|
||||
m_valueEdit->setEnabled(needsValue);
|
||||
m_valueLabel->setEnabled(needsValue);
|
||||
|
||||
// Auto-toggle filters: signatures → executable code, values → writable data
|
||||
m_execCheck->setChecked(isSig);
|
||||
m_writeCheck->setChecked(!isSig);
|
||||
}
|
||||
|
||||
void ScannerPanel::onConditionChanged(int /*index*/) {
|
||||
auto cond = (ScanCondition)m_condCombo->currentData().toInt();
|
||||
bool needsValue = (cond == ScanCondition::ExactValue);
|
||||
m_valueEdit->setEnabled(needsValue);
|
||||
m_valueLabel->setEnabled(needsValue);
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanClicked() {
|
||||
if (m_engine->isRunning()) {
|
||||
m_engine->abort();
|
||||
@@ -306,12 +349,14 @@ void ScannerPanel::onScanClicked() {
|
||||
|
||||
// Build request
|
||||
ScanRequest req = buildRequest();
|
||||
if (req.pattern.isEmpty())
|
||||
if (req.condition != ScanCondition::UnknownValue && req.pattern.isEmpty())
|
||||
return; // error already shown by buildRequest
|
||||
|
||||
m_lastScanMode = m_modeCombo->currentIndex();
|
||||
if (m_lastScanMode == 1)
|
||||
if (m_lastScanMode == 1) {
|
||||
m_lastValueType = (ValueType)m_typeCombo->currentData().toInt();
|
||||
m_lastCondition = req.condition;
|
||||
}
|
||||
m_lastPattern = req.pattern;
|
||||
|
||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||
@@ -336,29 +381,147 @@ ScanRequest ScannerPanel::buildRequest() {
|
||||
} else {
|
||||
// Value mode
|
||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||
if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return {};
|
||||
auto cond = (ScanCondition)m_condCombo->currentData().toInt();
|
||||
|
||||
// Comparison conditions on fresh scan → treat as unknown
|
||||
if (cond == ScanCondition::Changed || cond == ScanCondition::Unchanged ||
|
||||
cond == ScanCondition::Increased || cond == ScanCondition::Decreased) {
|
||||
cond = ScanCondition::UnknownValue;
|
||||
}
|
||||
|
||||
req.condition = cond;
|
||||
req.alignment = naturalAlignment(vt);
|
||||
req.valueSize = valueSizeForType(vt);
|
||||
|
||||
if (cond == ScanCondition::UnknownValue) {
|
||||
// No pattern needed — capture all aligned addresses
|
||||
req.maxResults = 10000000;
|
||||
} else {
|
||||
// Exact value mode
|
||||
if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.filterExecutable = m_execCheck->isChecked();
|
||||
req.filterWritable = m_writeCheck->isChecked();
|
||||
|
||||
if (m_structOnlyCheck->isChecked() && m_boundsGetter) {
|
||||
auto bounds = m_boundsGetter();
|
||||
if (bounds.size > 0) {
|
||||
req.startAddress = bounds.start;
|
||||
req.endAddress = bounds.start + bounds.size;
|
||||
}
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScannerPanel::runValueScanAndWait(ValueType valueType, const QString& value,
|
||||
bool filterExecutable, bool filterWritable,
|
||||
const QVector<AddressRange>& constrainRegions) {
|
||||
QVector<ScanResult> results;
|
||||
QString err;
|
||||
ScanRequest req;
|
||||
if (!serializeValue(valueType, value, req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return results;
|
||||
}
|
||||
req.alignment = naturalAlignment(valueType);
|
||||
req.filterExecutable = filterExecutable;
|
||||
req.filterWritable = filterWritable;
|
||||
req.constrainRegions = constrainRegions;
|
||||
|
||||
auto provider = m_providerGetter ? m_providerGetter() : nullptr;
|
||||
if (!provider) {
|
||||
m_statusLabel->setText(QStringLiteral("No provider (attach to a process or open a file first)"));
|
||||
return results;
|
||||
}
|
||||
if (m_engine->isRunning()) {
|
||||
m_statusLabel->setText(QStringLiteral("Scan already in progress"));
|
||||
return results;
|
||||
}
|
||||
|
||||
m_lastScanMode = 1;
|
||||
m_lastValueType = valueType;
|
||||
m_lastPattern = req.pattern;
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
m_statusLabel->setText(QStringLiteral("Scanning..."));
|
||||
|
||||
QEventLoop loop;
|
||||
connect(m_engine, &ScanEngine::finished, this, [&results, &loop](const QVector<ScanResult>& r) {
|
||||
results = r;
|
||||
loop.quit();
|
||||
}, Qt::SingleShotConnection);
|
||||
m_engine->start(provider, req);
|
||||
loop.exec();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScannerPanel::runPatternScanAndWait(const QString& pattern,
|
||||
bool filterExecutable, bool filterWritable,
|
||||
const QVector<AddressRange>& constrainRegions) {
|
||||
auto provider = m_providerGetter ? m_providerGetter() : nullptr;
|
||||
return runPatternScanAndWait(provider, pattern, filterExecutable, filterWritable, constrainRegions);
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScannerPanel::runPatternScanAndWait(std::shared_ptr<Provider> provider,
|
||||
const QString& pattern,
|
||||
bool filterExecutable, bool filterWritable,
|
||||
const QVector<AddressRange>& constrainRegions) {
|
||||
QVector<ScanResult> results;
|
||||
QString err;
|
||||
ScanRequest req;
|
||||
if (!parseSignature(pattern, req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return results;
|
||||
}
|
||||
req.alignment = 1;
|
||||
req.filterExecutable = filterExecutable;
|
||||
req.filterWritable = filterWritable;
|
||||
req.constrainRegions = constrainRegions;
|
||||
|
||||
if (!provider) {
|
||||
m_statusLabel->setText(QStringLiteral("No provider (attach to a process or open a file first)"));
|
||||
return results;
|
||||
}
|
||||
if (m_engine->isRunning()) {
|
||||
m_statusLabel->setText(QStringLiteral("Scan already in progress"));
|
||||
return results;
|
||||
}
|
||||
|
||||
m_lastScanMode = 0;
|
||||
m_lastPattern = req.pattern;
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
m_statusLabel->setText(QStringLiteral("Scanning..."));
|
||||
|
||||
QEventLoop loop;
|
||||
connect(m_engine, &ScanEngine::finished, this, [&results, &loop](const QVector<ScanResult>& r) {
|
||||
results = r;
|
||||
loop.quit();
|
||||
}, Qt::SingleShotConnection);
|
||||
m_engine->start(provider, req);
|
||||
loop.exec();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_results = std::move(results);
|
||||
|
||||
// Bytes are cached by the engine during scan.
|
||||
// Value mode: override with exact search pattern (engine caches raw chunk bytes).
|
||||
// Value mode (exact): override with exact search pattern (engine caches raw chunk bytes).
|
||||
// Unknown mode: keep engine-captured bytes as-is (they're the baseline).
|
||||
for (auto& r : m_results) {
|
||||
r.previousValue.clear();
|
||||
if (m_lastScanMode == 1)
|
||||
if (m_lastScanMode == 1 && m_lastCondition == ScanCondition::ExactValue)
|
||||
r.scanValue = m_lastPattern;
|
||||
}
|
||||
|
||||
@@ -372,8 +535,10 @@ void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
|
||||
}
|
||||
|
||||
int n = m_results.size();
|
||||
m_statusLabel->setText(QStringLiteral("%1 result%2")
|
||||
.arg(n).arg(n == 1 ? "" : "s"));
|
||||
if (m_lastCondition == ScanCondition::UnknownValue && n >= 10000000)
|
||||
m_statusLabel->setText(QStringLiteral("%1 results (capped — narrow with Re-scan)").arg(n));
|
||||
else
|
||||
m_statusLabel->setText(QStringLiteral("%1 result%2").arg(n).arg(n == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::populateTable(bool showPrevious) {
|
||||
@@ -425,29 +590,41 @@ void ScannerPanel::onUpdateClicked() {
|
||||
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
|
||||
// Build filter from current input field
|
||||
// Determine rescan condition
|
||||
ScanCondition cond = ScanCondition::ExactValue;
|
||||
if (m_lastScanMode == 1)
|
||||
cond = (ScanCondition)m_condCombo->currentData().toInt();
|
||||
|
||||
// For UnknownValue on rescan, just re-read all (update only, no filter)
|
||||
if (cond == ScanCondition::UnknownValue)
|
||||
cond = ScanCondition::ExactValue; // with empty filter = update only
|
||||
|
||||
// Build filter from current input field (only for ExactValue condition)
|
||||
QByteArray filterPattern, filterMask;
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode
|
||||
QString err;
|
||||
if (!m_patternEdit->text().trimmed().isEmpty()) {
|
||||
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return;
|
||||
if (cond == ScanCondition::ExactValue) {
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode
|
||||
QString err;
|
||||
if (!m_patternEdit->text().trimmed().isEmpty()) {
|
||||
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Value mode
|
||||
QString err;
|
||||
if (!m_valueEdit->text().trimmed().isEmpty()) {
|
||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return;
|
||||
} else {
|
||||
// Value mode — exact value filter
|
||||
QString err;
|
||||
if (!m_valueEdit->text().trimmed().isEmpty()) {
|
||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return;
|
||||
}
|
||||
m_lastValueType = vt;
|
||||
}
|
||||
m_lastValueType = vt;
|
||||
}
|
||||
}
|
||||
// Comparison conditions (Changed/Unchanged/Increased/Decreased) don't need a filter pattern
|
||||
|
||||
// Update last pattern so display uses the new value
|
||||
if (!filterPattern.isEmpty())
|
||||
@@ -460,7 +637,8 @@ void ScannerPanel::onUpdateClicked() {
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
|
||||
m_engine->startRescan(prov, m_results, readSize, filterPattern, filterMask);
|
||||
m_engine->startRescan(prov, m_results, readSize, cond, m_lastValueType,
|
||||
filterPattern, filterMask);
|
||||
}
|
||||
|
||||
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
||||
@@ -617,7 +795,7 @@ void ScannerPanel::onCellEdited(int row, int col) {
|
||||
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
|
||||
.arg(bytes.size())
|
||||
.arg(bytes.size() == 1 ? "" : "s")
|
||||
.arg(addr, 0, 16, QLatin1Char('0')).toUpper());
|
||||
.arg(QString::number(addr, 16).toUpper()));
|
||||
// Re-read and update cache
|
||||
m_resultTable->blockSignals(true);
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
@@ -666,12 +844,14 @@ void ScannerPanel::applyTheme(const Theme& theme) {
|
||||
theme.border.name(), theme.hover.name());
|
||||
m_modeCombo->setStyleSheet(comboStyle);
|
||||
m_typeCombo->setStyleSheet(comboStyle);
|
||||
m_condCombo->setStyleSheet(comboStyle);
|
||||
|
||||
// Labels
|
||||
QPalette lp;
|
||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_patternLabel->setPalette(lp);
|
||||
m_typeLabel->setPalette(lp);
|
||||
m_condLabel->setPalette(lp);
|
||||
m_valueLabel->setPalette(lp);
|
||||
m_statusLabel->setPalette(lp);
|
||||
|
||||
@@ -680,6 +860,7 @@ void ScannerPanel::applyTheme(const Theme& theme) {
|
||||
cp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_execCheck->setPalette(cp);
|
||||
m_writeCheck->setPalette(cp);
|
||||
m_structOnlyCheck->setPalette(cp);
|
||||
|
||||
// Buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
|
||||
@@ -34,6 +34,10 @@ public:
|
||||
using ProviderGetter = std::function<std::shared_ptr<Provider>()>;
|
||||
void setProviderGetter(ProviderGetter getter);
|
||||
|
||||
struct StructBounds { uint64_t start = 0; uint64_t size = 0; };
|
||||
using BoundsGetter = std::function<StructBounds()>;
|
||||
void setBoundsGetter(BoundsGetter getter);
|
||||
|
||||
void setEditorFont(const QFont& font);
|
||||
void applyTheme(const Theme& theme);
|
||||
|
||||
@@ -52,6 +56,24 @@ public:
|
||||
QPushButton* gotoButton() const { return m_gotoBtn; }
|
||||
QPushButton* copyButton() const { return m_copyBtn; }
|
||||
ScanEngine* engine() const { return m_engine; }
|
||||
QComboBox* condCombo() const { return m_condCombo; }
|
||||
QLabel* condLabel() const { return m_condLabel; }
|
||||
QCheckBox* structOnlyCheck() const { return m_structOnlyCheck; }
|
||||
|
||||
/** Run a value scan and block until done. For MCP / automation. Returns results; updates panel table. */
|
||||
QVector<ScanResult> runValueScanAndWait(ValueType valueType, const QString& value,
|
||||
bool filterExecutable = false, bool filterWritable = false,
|
||||
const QVector<AddressRange>& constrainRegions = {});
|
||||
|
||||
/** Run a pattern/signature scan and block until done. Pattern: space-separated hex bytes, e.g. "00 00 20 42 ?? ??". */
|
||||
QVector<ScanResult> runPatternScanAndWait(const QString& pattern,
|
||||
bool filterExecutable = false, bool filterWritable = false,
|
||||
const QVector<AddressRange>& constrainRegions = {});
|
||||
|
||||
/** Run pattern scan using the given provider (for MCP: use tab's provider so scan runs on the right tab). */
|
||||
QVector<ScanResult> runPatternScanAndWait(std::shared_ptr<Provider> provider, const QString& pattern,
|
||||
bool filterExecutable = false, bool filterWritable = false,
|
||||
const QVector<AddressRange>& constrainRegions = {});
|
||||
|
||||
signals:
|
||||
void goToAddress(uint64_t address);
|
||||
@@ -72,18 +94,23 @@ private:
|
||||
void populateTable(bool showPrevious);
|
||||
void updateComboWidth();
|
||||
|
||||
void onConditionChanged(int index);
|
||||
|
||||
// Input widgets
|
||||
QComboBox* m_modeCombo; // Signature / Value
|
||||
QLineEdit* m_patternEdit; // Signature pattern input
|
||||
QComboBox* m_typeCombo; // Value type dropdown
|
||||
QComboBox* m_condCombo; // Scan condition (Exact/Unknown/Changed/...)
|
||||
QLineEdit* m_valueEdit; // Value input
|
||||
QLabel* m_patternLabel;
|
||||
QLabel* m_typeLabel;
|
||||
QLabel* m_condLabel;
|
||||
QLabel* m_valueLabel;
|
||||
|
||||
// Filters
|
||||
QCheckBox* m_execCheck;
|
||||
QCheckBox* m_writeCheck;
|
||||
QCheckBox* m_structOnlyCheck;
|
||||
|
||||
// Actions
|
||||
QPushButton* m_scanBtn;
|
||||
@@ -100,9 +127,11 @@ private:
|
||||
// Engine
|
||||
ScanEngine* m_engine;
|
||||
ProviderGetter m_providerGetter;
|
||||
BoundsGetter m_boundsGetter;
|
||||
QVector<ScanResult> m_results;
|
||||
int m_lastScanMode = 0; // 0=signature, 1=value
|
||||
ValueType m_lastValueType = ValueType::Int32;
|
||||
ScanCondition m_lastCondition = ScanCondition::ExactValue;
|
||||
QByteArray m_lastPattern; // serialized search value
|
||||
int m_preRescanCount = 0; // result count before last rescan
|
||||
|
||||
|
||||
358
src/startpage.h
Normal file
@@ -0,0 +1,358 @@
|
||||
#pragma once
|
||||
#include "themes/thememanager.h"
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Single-widget start page: everything painted in paintEvent.
|
||||
// Zero CSS, zero Fusion conflicts, zero child-widget styling issues.
|
||||
|
||||
class StartPageWidget : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit StartPageWidget(QWidget* parent = nullptr) : QDialog(parent) {
|
||||
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
|
||||
setMouseTracking(true);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
m_search = new QLineEdit(this);
|
||||
m_search->setPlaceholderText("Search recent...");
|
||||
m_search->setFixedHeight(30);
|
||||
m_search->setMaximumWidth(330);
|
||||
m_search->addAction(QIcon(":/vsicons/search.svg"), QLineEdit::TrailingPosition);
|
||||
connect(m_search, &QLineEdit::textChanged, this, [this]{ buildGroups(); update(); });
|
||||
|
||||
loadEntries();
|
||||
buildGroups();
|
||||
applyTheme(ThemeManager::instance().current());
|
||||
}
|
||||
|
||||
void applyTheme(const Theme& t) {
|
||||
m_t = t;
|
||||
m_search->setStyleSheet(
|
||||
"QLineEdit { background: " + t.background.name() + "; color: " + t.text.name()
|
||||
+ "; border: 1px solid " + t.border.name()
|
||||
+ "; padding: 2px 8px; font-size: 13px; }"
|
||||
"QLineEdit:focus { border: 1px solid " + t.borderFocused.name() + "; }");
|
||||
update();
|
||||
}
|
||||
|
||||
signals:
|
||||
void openProject();
|
||||
void newClass();
|
||||
void importSource();
|
||||
void importXml();
|
||||
void importPdb();
|
||||
void continueClicked();
|
||||
void fileSelected(const QString& path);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const int LX = 48, TM = 36, RM = 32, GAP = 40, RW = 340;
|
||||
const int rpX = width() - RW - RM;
|
||||
const int lW = qMax(100, rpX - GAP - LX);
|
||||
|
||||
p.fillRect(rect(), m_t.background);
|
||||
|
||||
// ── Title ──
|
||||
int y = TM;
|
||||
QFont titleF = font(); titleF.setPixelSize(30); titleF.setWeight(QFont::Light);
|
||||
p.setFont(titleF); p.setPen(m_t.text);
|
||||
QFontMetrics titleFm(titleF);
|
||||
p.drawText(LX, y + titleFm.ascent(), "Reclass");
|
||||
y += titleFm.height() + 24;
|
||||
|
||||
// ── Headings (left + right at same y) ──
|
||||
QFont headF = font(); headF.setPixelSize(20); headF.setWeight(QFont::DemiBold);
|
||||
p.setFont(headF); QFontMetrics headFm(headF);
|
||||
p.drawText(LX, y + headFm.ascent(), "Open recent");
|
||||
int ry = y;
|
||||
p.drawText(rpX, ry + headFm.ascent(), "Get started");
|
||||
ry += headFm.height() + 14;
|
||||
y += headFm.height() + 14;
|
||||
|
||||
// ── Search bar (only child widget) ──
|
||||
m_search->setGeometry(LX, y, qMin(330, lW), 30);
|
||||
y += 46;
|
||||
m_listTop = y;
|
||||
|
||||
// ── Right panel ──
|
||||
drawCards(p, rpX, ry, RW);
|
||||
|
||||
// ── File list ──
|
||||
drawFileList(p, LX, lW);
|
||||
|
||||
// ── Border ──
|
||||
p.setPen(QPen(m_t.border, 1));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawRect(rect().adjusted(0, 0, -1, -1));
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
auto [z, i] = hitTest(e->pos());
|
||||
if (z != m_hz || i != m_hi) {
|
||||
m_hz = z; m_hi = i;
|
||||
setCursor(z != HZ_None ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void mousePressEvent(QMouseEvent* e) override {
|
||||
if (e->button() != Qt::LeftButton) return;
|
||||
auto [z, i] = hitTest(e->pos());
|
||||
if (z == HZ_Entry) emit fileSelected(m_filtered[i].path);
|
||||
if (z == HZ_Group) { m_groups[i].expanded = !m_groups[i].expanded; update(); }
|
||||
if (z == HZ_Card && i == 0) emit newClass();
|
||||
if (z == HZ_Card && i == 1) emit openProject();
|
||||
if (z == HZ_Card && i == 2) emit importSource();
|
||||
if (z == HZ_Card && i == 3) emit importXml();
|
||||
if (z == HZ_Card && i == 4) emit importPdb();
|
||||
if (z == HZ_Continue) emit continueClicked();
|
||||
}
|
||||
|
||||
void wheelEvent(QWheelEvent* e) override {
|
||||
m_scrollY = qBound(0, m_scrollY - e->angleDelta().y() / 2, m_maxScroll);
|
||||
update();
|
||||
}
|
||||
|
||||
void resizeEvent(QResizeEvent* e) override { QWidget::resizeEvent(e); update(); }
|
||||
void leaveEvent(QEvent*) override { m_hz = HZ_None; m_hi = -1; setCursor(Qt::ArrowCursor); update(); }
|
||||
void keyPressEvent(QKeyEvent* e) override { if (e->key() == Qt::Key_Escape) reject(); }
|
||||
|
||||
private:
|
||||
enum HZ { HZ_None, HZ_Entry, HZ_Group, HZ_Card, HZ_Continue };
|
||||
struct Hit { HZ zone; int idx; };
|
||||
|
||||
struct Entry {
|
||||
QString path, fileName, dirPath;
|
||||
QDateTime lastModified;
|
||||
bool isExample;
|
||||
};
|
||||
struct Group {
|
||||
QString name;
|
||||
bool expanded = true;
|
||||
QVector<int> entries;
|
||||
};
|
||||
|
||||
Theme m_t;
|
||||
QLineEdit* m_search;
|
||||
QVector<Entry> m_all, m_filtered;
|
||||
QVector<Group> m_groups;
|
||||
int m_scrollY = 0, m_maxScroll = 0, m_listTop = 0, m_contentH = 0;
|
||||
|
||||
HZ m_hz = HZ_None;
|
||||
int m_hi = -1;
|
||||
|
||||
// Hit rects populated during paint
|
||||
QVector<QPair<int, QRectF>> m_grpRects, m_entRects;
|
||||
QRectF m_cardR[5], m_contR;
|
||||
|
||||
void drawIcon(QPainter& p, const QString& path, int x, int y, int sz) {
|
||||
QIcon(path).paint(&p, x, y, sz, sz);
|
||||
}
|
||||
|
||||
// ── Data loading ──
|
||||
|
||||
void loadEntries() {
|
||||
m_all.clear();
|
||||
QSettings s("Reclass", "Reclass");
|
||||
for (const auto& path : s.value("recentFiles").toStringList()) {
|
||||
QFileInfo fi(path);
|
||||
if (!fi.exists()) continue;
|
||||
m_all.push_back(Entry{fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
|
||||
fi.lastModified(), false});
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
|
||||
#else
|
||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||
#endif
|
||||
for (const auto& fn : exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name))
|
||||
m_all.push_back(Entry{exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
|
||||
QFileInfo(exDir.filePath(fn)).lastModified(), true});
|
||||
}
|
||||
|
||||
void buildGroups() {
|
||||
QString f = m_search->text().trimmed().toLower();
|
||||
m_filtered.clear();
|
||||
for (const auto& e : m_all)
|
||||
if (f.isEmpty() || e.fileName.toLower().contains(f) || e.dirPath.toLower().contains(f))
|
||||
m_filtered.append(e);
|
||||
|
||||
QDate today = QDate::currentDate();
|
||||
QVector<int> bk[6];
|
||||
for (int i = 0; i < m_filtered.size(); i++) {
|
||||
auto& e = m_filtered[i];
|
||||
if (e.isExample) { bk[5].append(i); continue; }
|
||||
int d = e.lastModified.date().daysTo(today);
|
||||
if (d == 0) bk[0].append(i);
|
||||
else if (d == 1) bk[1].append(i);
|
||||
else if (d < 7) bk[2].append(i);
|
||||
else if (e.lastModified.date().month() == today.month()
|
||||
&& e.lastModified.date().year() == today.year()) bk[3].append(i);
|
||||
else bk[4].append(i);
|
||||
}
|
||||
static const char* names[] = {"Today","Yesterday","This week","This month","Older","Examples"};
|
||||
m_groups.clear();
|
||||
for (int i = 0; i < 6; i++)
|
||||
if (!bk[i].isEmpty()) m_groups.push_back(Group{names[i], true, bk[i]});
|
||||
m_scrollY = 0;
|
||||
}
|
||||
|
||||
// ── Drawing ──
|
||||
|
||||
void drawCards(QPainter& p, int x, int y, int w) {
|
||||
struct C { const char* icon; const char* title; const char* desc; };
|
||||
static const C cards[] = {
|
||||
{":/vsicons/symbol-structure.svg", "New Class", "Start a new binary class definition"},
|
||||
{":/vsicons/folder-opened.svg", "Open project", "Open an existing .rcx project"},
|
||||
{":/vsicons/file-binary.svg", "Import from Source", "Import C/C++ header or source file"},
|
||||
{":/vsicons/code.svg", "Import ReClass XML", "Import from ReClass .xml format"},
|
||||
{":/vsicons/debug.svg", "Import PDB", "Import types from a .pdb symbol file"}
|
||||
};
|
||||
|
||||
const int N = 5, CH = 84, panelH = N * CH;
|
||||
|
||||
// Sharp-cornered panel background
|
||||
p.save();
|
||||
p.setClipRect(QRectF(x, y, w, panelH));
|
||||
p.fillRect(x, y, w, panelH, m_t.background);
|
||||
|
||||
for (int i = 0; i < N; i++) {
|
||||
int cy = y + i * CH;
|
||||
QRectF cr(x, cy, w, CH);
|
||||
m_cardR[i] = cr;
|
||||
bool hov = (m_hz == HZ_Card && m_hi == i);
|
||||
|
||||
if (hov) {
|
||||
p.fillRect(cr, m_t.hover);
|
||||
p.fillRect(QRectF(x, cy, 3, CH), m_t.indHoverSpan);
|
||||
}
|
||||
|
||||
// Icon (32px, centered vertically)
|
||||
int iconSz = 32;
|
||||
drawIcon(p, cards[i].icon, x + 24, cy + (CH - iconSz) / 2, iconSz);
|
||||
|
||||
// Title + description block, centered vertically
|
||||
int tx = x + 24 + iconSz + 16;
|
||||
QFont tf = font(); tf.setPixelSize(15);
|
||||
QFont df = font(); df.setPixelSize(12);
|
||||
QFontMetrics tfm(tf), dfm(df);
|
||||
int blockH = tfm.height() + 5 + dfm.height();
|
||||
int by = cy + (CH - blockH) / 2;
|
||||
|
||||
p.setFont(tf); p.setPen(m_t.text);
|
||||
p.drawText(tx, by + tfm.ascent(), cards[i].title);
|
||||
p.setFont(df); p.setPen(m_t.textDim);
|
||||
p.drawText(tx, by + tfm.height() + 5 + dfm.ascent(), cards[i].desc);
|
||||
}
|
||||
|
||||
p.restore();
|
||||
|
||||
// "Continue →" centered under the panel
|
||||
int cy = y + panelH + 8;
|
||||
QFont lf = font(); lf.setPixelSize(13);
|
||||
if (m_hz == HZ_Continue) lf.setUnderline(true);
|
||||
p.setFont(lf); p.setPen(m_t.indHoverSpan);
|
||||
QFontMetrics lfm(lf);
|
||||
QString ct = QStringLiteral("Continue \u2192");
|
||||
int cw = lfm.horizontalAdvance(ct);
|
||||
m_contR = QRectF(x + (w - cw) / 2, cy, cw, lfm.height());
|
||||
p.drawText(int(m_contR.x()), cy + lfm.ascent(), ct);
|
||||
}
|
||||
|
||||
void drawFileList(QPainter& p, int x, int w) {
|
||||
int listH = height() - 24 - m_listTop;
|
||||
p.save();
|
||||
p.setClipRect(x, m_listTop, w, listH);
|
||||
|
||||
int fy = m_listTop - m_scrollY;
|
||||
m_grpRects.clear();
|
||||
m_entRects.clear();
|
||||
|
||||
for (int gi = 0; gi < m_groups.size(); gi++) {
|
||||
auto& g = m_groups[gi];
|
||||
if (gi > 0) fy += 15;
|
||||
|
||||
// Group header
|
||||
m_grpRects.emplaceBack(gi, QRectF(x, fy, w, 28));
|
||||
p.setPen(Qt::NoPen); p.setBrush(m_t.text);
|
||||
int triX = x + 8, triY = fy + 11;
|
||||
QPolygonF tri;
|
||||
if (g.expanded) tri << QPointF(triX,triY) << QPointF(triX+6,triY) << QPointF(triX+3,triY+6);
|
||||
else tri << QPointF(triX,triY) << QPointF(triX+6,triY+3) << QPointF(triX,triY+6);
|
||||
p.drawPolygon(tri);
|
||||
|
||||
QFont gf = font(); gf.setPixelSize(13);
|
||||
p.setFont(gf); p.setPen(m_t.text);
|
||||
p.drawText(triX + 14, fy + 14 + QFontMetrics(gf).ascent() / 2 - 1, g.name);
|
||||
fy += 28;
|
||||
|
||||
if (!g.expanded) continue;
|
||||
|
||||
for (int ei : g.entries) {
|
||||
auto& e = m_filtered[ei];
|
||||
QRectF er(x, fy, w, 52);
|
||||
m_entRects.emplaceBack(ei, er);
|
||||
if (m_hz == HZ_Entry && m_hi == ei) p.fillRect(er, m_t.hover);
|
||||
|
||||
drawIcon(p, e.isExample ? ":/vsicons/book.svg" : ":/vsicons/symbol-structure.svg",
|
||||
x + 24, fy + 17, 18);
|
||||
|
||||
int tx = x + 52, avail = w - 64;
|
||||
QFont nf = font(); nf.setPixelSize(14);
|
||||
p.setFont(nf); p.setPen(m_t.text);
|
||||
QFontMetrics nm(nf);
|
||||
int ny = fy + 8;
|
||||
p.drawText(tx, ny + nm.ascent(),
|
||||
nm.elidedText(e.fileName, Qt::ElideMiddle, avail * 0.65));
|
||||
|
||||
if (!e.isExample) {
|
||||
p.setPen(m_t.textDim);
|
||||
QString dt = e.lastModified.toString("M/d/yyyy h:mm AP");
|
||||
p.drawText(x + w - 12 - nm.horizontalAdvance(dt), ny + nm.ascent(), dt);
|
||||
}
|
||||
|
||||
QFont pf = font(); pf.setPixelSize(12);
|
||||
p.setFont(pf); p.setPen(m_t.textDim);
|
||||
QFontMetrics pm(pf);
|
||||
p.drawText(tx, ny + nm.height() + 4 + pm.ascent(),
|
||||
pm.elidedText(e.dirPath, Qt::ElideMiddle, avail));
|
||||
fy += 52;
|
||||
}
|
||||
}
|
||||
|
||||
m_contentH = fy + m_scrollY - m_listTop;
|
||||
m_maxScroll = qMax(0, m_contentH - listH);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
// ── Hit testing ──
|
||||
|
||||
Hit hitTest(QPoint pos) const {
|
||||
for (int i = 0; i < 5; i++)
|
||||
if (m_cardR[i].contains(pos)) return {HZ_Card, i};
|
||||
if (m_contR.contains(pos)) return {HZ_Continue, 0};
|
||||
if (pos.y() >= m_listTop && pos.y() < height() - 24) {
|
||||
for (const auto& [gi, r] : m_grpRects)
|
||||
if (r.contains(pos)) return {HZ_Group, gi};
|
||||
for (const auto& [ei, r] : m_entRects)
|
||||
if (r.contains(pos)) return {HZ_Entry, ei};
|
||||
}
|
||||
return {HZ_None, -1};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
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
@@ -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
|
||||
191
src/symbolstore.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
#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::addModuleTypeIndices(const QString& moduleName,
|
||||
const QHash<QString, uint32_t>& nameToTypeIndex) {
|
||||
QString canonical = resolveAlias(moduleName);
|
||||
auto it = m_modules.find(canonical);
|
||||
if (it == m_modules.end()) return;
|
||||
it->nameToTypeIndex = nameToTypeIndex;
|
||||
}
|
||||
|
||||
uint32_t SymbolStore::typeIndexForSymbol(const QString& qualifiedSymbol) const {
|
||||
int bangIdx = qualifiedSymbol.indexOf('!');
|
||||
if (bangIdx <= 0 || bangIdx >= qualifiedSymbol.size() - 1)
|
||||
return 0;
|
||||
QString modPart = qualifiedSymbol.left(bangIdx);
|
||||
QString symPart = qualifiedSymbol.mid(bangIdx + 1);
|
||||
QString canonical = resolveAlias(modPart);
|
||||
auto modIt = m_modules.find(canonical);
|
||||
if (modIt == m_modules.end()) return 0;
|
||||
return modIt->nameToTypeIndex.value(symPart, 0);
|
||||
}
|
||||
|
||||
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
|
||||
105
src/symbolstore.h
Normal file
@@ -0,0 +1,105 @@
|
||||
#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;
|
||||
QHash<QString, uint32_t> nameToTypeIndex; // symbol name → TPI typeIndex (0 = no type info)
|
||||
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);
|
||||
|
||||
// Store symbol→typeIndex mapping for a previously-added module.
|
||||
// Called after addModule with the typeIndex data from PdbSymbol records.
|
||||
void addModuleTypeIndices(const QString& moduleName,
|
||||
const QHash<QString, uint32_t>& nameToTypeIndex);
|
||||
|
||||
// Look up the TPI typeIndex for a qualified symbol (e.g. "ntdll!g_pShimEngineModule").
|
||||
// Returns 0 if not found or no type info available.
|
||||
uint32_t typeIndexForSymbol(const QString& qualifiedSymbol) const;
|
||||
|
||||
// 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
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#505C74",
|
||||
"textMuted": "#384258",
|
||||
"textFaint": "#2C3448",
|
||||
"hover": "#121720",
|
||||
"selected": "#121720",
|
||||
"hover": "#181E2A",
|
||||
"selected": "#1A2D4A",
|
||||
"selection": "#1A2038",
|
||||
"syntaxKeyword": "#5688C0",
|
||||
"syntaxNumber": "#90B480",
|
||||
|
||||
32
src/themes/defaults/modern.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Modern",
|
||||
"background": "#0e1117",
|
||||
"backgroundAlt": "#12151c",
|
||||
"surface": "#181d27",
|
||||
"border": "#1e2533",
|
||||
"borderFocused": "#4fc3f7",
|
||||
"button": "#1e2433",
|
||||
"text": "#a8bbd0",
|
||||
"textDim": "#7a8fa8",
|
||||
"textMuted": "#566278",
|
||||
"textFaint": "#3d4d6a",
|
||||
"hover": "#1e2433",
|
||||
"selected": "#232a3a",
|
||||
"selection": "#1a4a5e",
|
||||
"syntaxKeyword": "#9d8cff",
|
||||
"syntaxNumber": "#f0c060",
|
||||
"syntaxString": "#26c6b3",
|
||||
"syntaxComment": "#566278",
|
||||
"syntaxPreproc": "#f472b6",
|
||||
"syntaxType": "#4fc3f7",
|
||||
"indHoverSpan": "#f0c060",
|
||||
"indCmdPill": "#12151c",
|
||||
"indDataChanged": "#6bda8a",
|
||||
"indHeatCold": "#f0c060",
|
||||
"indHeatWarm": "#e8946a",
|
||||
"indHeatHot": "#ff6b6b",
|
||||
"indHintGreen": "#2a5e3a",
|
||||
"markerPtr": "#ff6b6b",
|
||||
"markerCycle": "#f0c060",
|
||||
"markerError": "#3a1a1a"
|
||||
}
|
||||
32
src/themes/defaults/tw.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Light",
|
||||
"background": "#e8e8ec",
|
||||
"backgroundAlt": "#dcdce0",
|
||||
"surface": "#d4d4d8",
|
||||
"border": "#b8b8be",
|
||||
"borderFocused": "#6870a0",
|
||||
"button": "#ccccd0",
|
||||
"text": "#1b1b22",
|
||||
"textDim": "#5c5c68",
|
||||
"textMuted": "#6a6a78",
|
||||
"textFaint": "#8a8a94",
|
||||
"hover": "#d8d8de",
|
||||
"selected": "#d0d0d8",
|
||||
"selection": "#b4c8e8",
|
||||
"syntaxKeyword": "#4455aa",
|
||||
"syntaxNumber": "#2a7a4c",
|
||||
"syntaxString": "#9a4040",
|
||||
"syntaxComment": "#6a7a6a",
|
||||
"syntaxPreproc": "#787880",
|
||||
"syntaxType": "#2e7a8a",
|
||||
"indHoverSpan": "#5a68a0",
|
||||
"indCmdPill": "#dcdce0",
|
||||
"indDataChanged": "#2a7a4c",
|
||||
"indHeatCold": "#6a6a30",
|
||||
"indHeatWarm": "#a06828",
|
||||
"indHeatHot": "#b83030",
|
||||
"indHintGreen": "#387a44",
|
||||
"markerPtr": "#b83030",
|
||||
"markerCycle": "#9a7010",
|
||||
"markerError": "#e8c8c8"
|
||||
}
|
||||
@@ -15,8 +15,8 @@
|
||||
"selection": "#21213A",
|
||||
"syntaxKeyword": "#AA9565",
|
||||
"syntaxNumber": "#AAA98C",
|
||||
"syntaxString": "#6B3B21",
|
||||
"syntaxComment": "#464646",
|
||||
"syntaxString": "#C0825A",
|
||||
"syntaxComment": "#8A8878",
|
||||
"syntaxPreproc": "#AA9565",
|
||||
"syntaxType": "#6B959F",
|
||||
"indHoverSpan": "#AA9565",
|
||||
@@ -25,8 +25,8 @@
|
||||
"indHeatCold": "#C4A44A",
|
||||
"indHeatWarm": "#AA9565",
|
||||
"indHeatHot": "#A05040",
|
||||
"indHintGreen": "#464646",
|
||||
"markerPtr": "#6B3B21",
|
||||
"indHintGreen": "#688A58",
|
||||
"markerPtr": "#B85A42",
|
||||
"markerCycle": "#AA9565",
|
||||
"markerError": "#3C2121"
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
||||
|
||||
// ── File info ──
|
||||
m_fileInfoLabel = new QLabel;
|
||||
m_fileInfoLabel->setStyleSheet(QStringLiteral("color: #666; font-size: 10px; padding: 0 0 4px 0;"));
|
||||
m_fileInfoLabel->setStyleSheet(QStringLiteral("color: %1; font-size: 10px; padding: 0 0 4px 0;")
|
||||
.arg(tm.current().textDim.name()));
|
||||
QString path = tm.themeFilePath(themeIndex);
|
||||
m_fileInfoLabel->setText(path.isEmpty()
|
||||
? QStringLiteral("Built-in theme (edits save as user copy)")
|
||||
@@ -109,7 +110,8 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
||||
|
||||
auto* hexLbl = new QLabel;
|
||||
hexLbl->setFixedWidth(60);
|
||||
hexLbl->setStyleSheet(QStringLiteral("color: #aaa; font-size: 10px;"));
|
||||
hexLbl->setStyleSheet(QStringLiteral("color: %1; font-size: 10px;")
|
||||
.arg(tm.current().textMuted.name()));
|
||||
row->addWidget(hexLbl);
|
||||
|
||||
row->addStretch();
|
||||
|
||||
@@ -33,7 +33,12 @@ ThemeManager::ThemeManager() {
|
||||
// ── Load built-in themes from JSON files next to the executable ──
|
||||
|
||||
QString ThemeManager::builtInDir() const {
|
||||
#ifdef Q_OS_MACOS
|
||||
// In a macOS .app bundle, resources live in Contents/Resources, not Contents/MacOS
|
||||
return QCoreApplication::applicationDirPath() + "/../Resources/themes";
|
||||
#else
|
||||
return QCoreApplication::applicationDirPath() + "/themes";
|
||||
#endif
|
||||
}
|
||||
|
||||
void ThemeManager::loadBuiltInThemes() {
|
||||
|
||||
115
src/titlebar.cpp
@@ -1,8 +1,10 @@
|
||||
#include "titlebar.h"
|
||||
#include "themes/thememanager.h"
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QStyle>
|
||||
#include <QTimer>
|
||||
#include <QWindow>
|
||||
|
||||
namespace rcx {
|
||||
@@ -24,11 +26,23 @@ TitleBarWidget::TitleBarWidget(QWidget* parent)
|
||||
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
layout->addWidget(m_appLabel);
|
||||
|
||||
// Menu bar
|
||||
// Menu bar — hidden on Linux; visible on Windows.
|
||||
// On Linux, QMenuBar inside a custom widget collapses all items into an
|
||||
// extension popup. We keep it hidden and mirror its menus as QToolButtons
|
||||
// via finalizeMenuBar() after createMenus() populates it.
|
||||
m_menuBar = new QMenuBar(this);
|
||||
m_menuBar->setNativeMenuBar(false);
|
||||
#ifdef __linux__
|
||||
m_useToolButtons = true;
|
||||
m_menuBar->hide();
|
||||
m_menuBtnLayout = new QHBoxLayout;
|
||||
m_menuBtnLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_menuBtnLayout->setSpacing(0);
|
||||
layout->addLayout(m_menuBtnLayout);
|
||||
#else
|
||||
m_menuBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||
layout->addWidget(m_menuBar);
|
||||
#endif
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
@@ -74,17 +88,37 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
// App label
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.textDim.name()));
|
||||
.arg(theme.text.name()));
|
||||
|
||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
||||
// Menu bar palette — all roles used by MenuBarStyle, so live theme
|
||||
// switches don't rely on app-palette inheritance (which can stall
|
||||
// once setPalette has been called on a widget).
|
||||
{
|
||||
QPalette mbPal = m_menuBar->palette();
|
||||
mbPal.setColor(QPalette::Window, theme.background);
|
||||
mbPal.setColor(QPalette::Button, theme.background);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.textDim);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.text);
|
||||
mbPal.setColor(QPalette::Text, theme.text);
|
||||
mbPal.setColor(QPalette::Highlight, theme.selected);
|
||||
mbPal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||
mbPal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
mbPal.setColor(QPalette::Dark, theme.border);
|
||||
mbPal.setColor(QPalette::Mid, theme.hover);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
|
||||
// Propagate to existing QMenu children so dropdown popups update too
|
||||
for (auto* menu : m_menuBar->findChildren<QMenu*>()) {
|
||||
QPalette mp = menu->palette();
|
||||
mp.setColor(QPalette::Window, theme.background);
|
||||
mp.setColor(QPalette::WindowText, theme.text);
|
||||
mp.setColor(QPalette::Text, theme.text);
|
||||
mp.setColor(QPalette::Highlight, theme.selected);
|
||||
mp.setColor(QPalette::Link, theme.indHoverSpan);
|
||||
mp.setColor(QPalette::AlternateBase, theme.surface);
|
||||
mp.setColor(QPalette::Dark, theme.border);
|
||||
menu->setPalette(mp);
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome buttons
|
||||
@@ -95,10 +129,21 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
m_btnMin->setStyleSheet(btnStyle);
|
||||
m_btnMax->setStyleSheet(btnStyle);
|
||||
|
||||
// Close button: red hover
|
||||
// Linux menu tool buttons
|
||||
if (m_useToolButtons) {
|
||||
QString menuBtnStyle = QStringLiteral(
|
||||
"QToolButton { background: transparent; border: none; padding: 0 8px; color: %1; }"
|
||||
"QToolButton:hover { background: %2; }"
|
||||
"QToolButton::menu-indicator { image: none; }")
|
||||
.arg(theme.text.name(), theme.hover.name());
|
||||
for (auto* btn : m_menuButtons)
|
||||
btn->setStyleSheet(menuBtnStyle);
|
||||
}
|
||||
|
||||
// Close button: themed red hover
|
||||
m_btnClose->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { background: transparent; border: none; }"
|
||||
"QToolButton:hover { background: #c42b1c; }"));
|
||||
"QToolButton:hover { background: %1; }").arg(theme.indHeatHot.name()));
|
||||
|
||||
update();
|
||||
}
|
||||
@@ -107,12 +152,14 @@ void TitleBarWidget::setShowIcon(bool show) {
|
||||
if (show) {
|
||||
m_appLabel->setText(QString());
|
||||
m_appLabel->setPixmap(QIcon(":/icons/class.png").pixmap(24, 24));
|
||||
setFixedHeight(34);
|
||||
} else {
|
||||
m_appLabel->setPixmap(QPixmap());
|
||||
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(m_theme.textDim.name()));
|
||||
.arg(m_theme.text.name()));
|
||||
setFixedHeight(32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +188,58 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
||||
action->setText("&" + result);
|
||||
}
|
||||
}
|
||||
// Sync tool button labels on Linux
|
||||
if (m_useToolButtons) {
|
||||
auto actions = m_menuBar->actions();
|
||||
for (int i = 0; i < m_menuButtons.size() && i < actions.size(); ++i)
|
||||
m_menuButtons[i]->setText(actions[i]->text());
|
||||
}
|
||||
}
|
||||
|
||||
void TitleBarWidget::finalizeMenuBar() {
|
||||
if (!m_useToolButtons) return;
|
||||
// Create a QToolButton for each top-level menu in the hidden QMenuBar
|
||||
for (auto* action : m_menuBar->actions()) {
|
||||
if (!action->menu()) continue;
|
||||
auto* btn = new QToolButton(this);
|
||||
btn->setText(action->text());
|
||||
btn->setMenu(action->menu());
|
||||
btn->setPopupMode(QToolButton::InstantPopup);
|
||||
btn->setAutoRaise(true);
|
||||
btn->setFocusPolicy(Qt::NoFocus);
|
||||
btn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||
btn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { background: transparent; border: none; padding: 0 8px; }"
|
||||
"QToolButton:hover { background: %1; }"
|
||||
"QToolButton::menu-indicator { image: none; }")
|
||||
.arg(m_theme.hover.name()));
|
||||
btn->installEventFilter(this);
|
||||
btn->menu()->installEventFilter(this);
|
||||
m_menuBtnLayout->addWidget(btn);
|
||||
m_menuButtons.append(btn);
|
||||
}
|
||||
}
|
||||
|
||||
bool TitleBarWidget::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (!m_useToolButtons) return QWidget::eventFilter(obj, event);
|
||||
|
||||
// Watch for mouse movement inside an open QMenu — if the cursor moves
|
||||
// over a sibling menu button, close this menu and open the other.
|
||||
if (event->type() == QEvent::MouseMove) {
|
||||
auto* menu = qobject_cast<QMenu*>(obj);
|
||||
if (!menu || !menu->isVisible()) return false;
|
||||
QPoint globalPos = QCursor::pos();
|
||||
for (auto* btn : m_menuButtons) {
|
||||
if (btn->menu() == menu) continue;
|
||||
QRect btnRect(btn->mapToGlobal(QPoint(0, 0)), btn->size());
|
||||
if (btnRect.contains(globalPos)) {
|
||||
menu->close();
|
||||
QTimer::singleShot(0, btn, [btn]() { btn->showMenu(); });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return QWidget::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void TitleBarWidget::updateMaximizeIcon() {
|
||||
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
void setShowIcon(bool show);
|
||||
void setMenuBarTitleCase(bool titleCase);
|
||||
bool menuBarTitleCase() const { return m_titleCase; }
|
||||
void finalizeMenuBar();
|
||||
|
||||
void updateMaximizeIcon();
|
||||
|
||||
@@ -25,16 +26,20 @@ protected:
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
|
||||
private:
|
||||
QLabel* m_appLabel = nullptr;
|
||||
QMenuBar* m_menuBar = nullptr;
|
||||
QHBoxLayout* m_menuBtnLayout = nullptr;
|
||||
QVector<QToolButton*> m_menuButtons;
|
||||
QToolButton* m_btnMin = nullptr;
|
||||
QToolButton* m_btnMax = nullptr;
|
||||
QToolButton* m_btnClose = nullptr;
|
||||
|
||||
Theme m_theme;
|
||||
bool m_titleCase = false;
|
||||
bool m_useToolButtons = false;
|
||||
|
||||
QToolButton* makeChromeButton(const QString& iconPath);
|
||||
void toggleMaximize();
|
||||
|
||||
519
src/typeinfer.h
Normal file
@@ -0,0 +1,519 @@
|
||||
#pragma once
|
||||
#include <QVector>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Hints from value history (optional, improves accuracy) ──
|
||||
|
||||
struct InferHints {
|
||||
const uint8_t* minObserved = nullptr; // raw bytes, same len as data
|
||||
const uint8_t* maxObserved = nullptr;
|
||||
bool monotonic = false; // value only increases or only decreases
|
||||
bool neverChanged = false; // identical across all samples
|
||||
int sampleCount = 0; // 0 = no history
|
||||
int ptrSize = 8;
|
||||
};
|
||||
|
||||
// ── Suggestion result ──
|
||||
|
||||
struct TypeSuggestion {
|
||||
QVector<NodeKind> kinds; // size==1: convert, size>1: uniform split
|
||||
int score = 0; // 0-100 feature ratio (passed / checked × 100)
|
||||
int strength = 0; // 0=hidden, 1=weak, 2=moderate, 3=strong
|
||||
};
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
QVector<TypeSuggestion> inferTypes(
|
||||
const uint8_t* data, int len,
|
||||
const InferHints& hints = {},
|
||||
int maxResults = 3);
|
||||
|
||||
// Format top suggestion as short type label (e.g. "ptr64", "int32_t×2")
|
||||
inline QString formatHint(const TypeSuggestion& s) {
|
||||
if (s.kinds.isEmpty()) return {};
|
||||
const char* name = kindMeta(s.kinds[0])->typeName;
|
||||
return (s.kinds.size() == 1)
|
||||
? QString::fromLatin1(name)
|
||||
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
|
||||
}
|
||||
|
||||
// ── Implementation (header-only) ──
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline uint32_t loadU32(const uint8_t* p) {
|
||||
uint32_t v; std::memcpy(&v, p, 4); return v;
|
||||
}
|
||||
inline uint64_t loadU64(const uint8_t* p) {
|
||||
uint64_t v; std::memcpy(&v, p, 8); return v;
|
||||
}
|
||||
inline uint16_t loadU16(const uint8_t* p) {
|
||||
uint16_t v; std::memcpy(&v, p, 2); return v;
|
||||
}
|
||||
inline float loadF32(const uint8_t* p) {
|
||||
float v; std::memcpy(&v, p, 4); return v;
|
||||
}
|
||||
inline double loadF64(const uint8_t* p) {
|
||||
double v; std::memcpy(&v, p, 8); return v;
|
||||
}
|
||||
|
||||
inline bool allZero(const uint8_t* p, int n) {
|
||||
for (int i = 0; i < n; ++i) if (p[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline int popcount32(uint32_t v) {
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_popcount(v);
|
||||
#else
|
||||
int c = 0; while (v) { v &= v - 1; ++c; } return c;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool isPrintable(uint8_t c) {
|
||||
return c >= 0x20 && c <= 0x7E;
|
||||
}
|
||||
|
||||
// ── Float feature checker ──
|
||||
// Returns features passed out of features checked (as pair)
|
||||
|
||||
struct FeatureResult { int passed; int checked; };
|
||||
|
||||
inline bool isGoodFloat(uint32_t bits) {
|
||||
uint32_t exp = (bits >> 23) & 0xFF;
|
||||
if (exp == 0xFF) return false; // inf/nan
|
||||
if (exp == 0 && (bits & 0x7FFFFF)) return false; // denormal
|
||||
float f; std::memcpy(&f, &bits, 4);
|
||||
double af = std::fabs((double)f);
|
||||
return f == 0.0f || (af >= 1e-6 && af <= 1e7);
|
||||
}
|
||||
|
||||
inline FeatureResult countFloatFeatures(uint32_t cur,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
int passed = 0, checked = 4;
|
||||
float f; std::memcpy(&f, &cur, 4);
|
||||
|
||||
// Feature 1: finite
|
||||
passed += std::isfinite((double)f) ? 1 : 0;
|
||||
// Feature 2: non-denormal (exponent > 0 or value is ±0)
|
||||
uint32_t exp = (cur >> 23) & 0xFF;
|
||||
passed += (exp > 0 || (cur & 0x7FFFFFFF) == 0) ? 1 : 0;
|
||||
// Feature 3: reasonable range
|
||||
double af = std::fabs((double)f);
|
||||
passed += (f == 0.0f || (af >= 1e-6 && af <= 1e7)) ? 1 : 0;
|
||||
// Feature 4: has fractional part (not just a reinterpreted integer)
|
||||
float ip; double frac = std::fabs((double)std::modf(f, &ip));
|
||||
passed += (frac > 0.0001) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 4;
|
||||
uint32_t minBits = loadU32(minP), maxBits = loadU32(maxP);
|
||||
// Feature 5-6: min/max are also valid floats
|
||||
passed += isGoodFloat(minBits) ? 1 : 0;
|
||||
passed += isGoodFloat(maxBits) ? 1 : 0;
|
||||
// Feature 7: field changes
|
||||
passed += (minBits != maxBits) ? 1 : 0;
|
||||
// Feature 8: range is game-plausible
|
||||
float fmin, fmax;
|
||||
std::memcpy(&fmin, &minBits, 4);
|
||||
std::memcpy(&fmax, &maxBits, 4);
|
||||
double range = std::fabs((double)fmax - (double)fmin);
|
||||
passed += (range < 1e6) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Integer feature checker ──
|
||||
|
||||
inline FeatureResult countIntFeatures(uint32_t val,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
// Hard reject: zero and sentinel are never useful integers
|
||||
if (val == 0 || val == 0xFFFFFFFF)
|
||||
return {0, 3};
|
||||
|
||||
int passed = 0, checked = 3;
|
||||
int32_t sv = (int32_t)val;
|
||||
|
||||
// Feature 1: non-zero and not sentinel (always passes after hard reject)
|
||||
passed += 1;
|
||||
// Feature 2: small absolute value
|
||||
passed += (val <= 1000000u || (uint32_t)(sv + 1000000) <= 2000000u) ? 1 : 0;
|
||||
// Feature 3: fits int16 range
|
||||
passed += (sv >= -32768 && sv <= 32767) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 3;
|
||||
uint32_t minV = loadU32(minP), maxV = loadU32(maxP);
|
||||
// Feature 4: min/max in reasonable range
|
||||
passed += (minV <= 1000000u && maxV <= 1000000u) ? 1 : 0;
|
||||
// Feature 5: monotonic (counter/timer)
|
||||
passed += h.monotonic ? 1 : 0;
|
||||
// Feature 6: field varies
|
||||
passed += (minV != maxV) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Flags feature checker ──
|
||||
|
||||
inline FeatureResult countFlagFeatures(uint32_t val,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
int passed = 0, checked = 2;
|
||||
int pc = popcount32(val);
|
||||
|
||||
// Feature 1: sparse bits (1-3 set)
|
||||
passed += (pc >= 1 && pc <= 3) ? 1 : 0;
|
||||
// Feature 2: not a small sequential integer (flags are usually not 1,2,3...)
|
||||
passed += (val > 256 || (val & (val - 1)) != 0) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 3;
|
||||
uint32_t minV = loadU32(minP), maxV = loadU32(maxP);
|
||||
// Feature 3: XOR of min/max has low popcount (specific bits toggle)
|
||||
passed += (popcount32(minV ^ maxV) <= 4) ? 1 : 0;
|
||||
// Feature 4: field varies
|
||||
passed += (minV != maxV) ? 1 : 0;
|
||||
// Feature 5: max is superset of min bits
|
||||
passed += ((minV & maxV) == minV) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Pointer feature checker ──
|
||||
|
||||
inline FeatureResult countPtrFeatures64(uint64_t val) {
|
||||
// Hard reject: common sentinel values
|
||||
if (val == 0 || val == 0xFFFFFFFFFFFFFFFFULL || val == 0x00000000FFFFFFFFULL)
|
||||
return {0, 5};
|
||||
|
||||
// Hard reject: non-canonical address — impossible to dereference on x64
|
||||
// User-mode: 0x0000000000000000 – 0x00007FFFFFFFFFFF
|
||||
// Kernel: 0xFFFF800000000000 – 0xFFFFFFFFFFFFFFFF
|
||||
if (val > 0x00007FFFFFFFFFFFULL && val < 0xFFFF800000000000ULL)
|
||||
return {0, 5};
|
||||
|
||||
int passed = 0, checked = 5;
|
||||
// Feature 1: aligned to 8 (heap/vtable allocations)
|
||||
passed += ((val & 7) == 0) ? 1 : 0;
|
||||
// Feature 2: above null guard pages (real addresses >= 64KB)
|
||||
passed += (val >= 0x10000) ? 1 : 0;
|
||||
// Feature 3: has upper 32 bits (real 64-bit address, not a small constant)
|
||||
passed += ((val >> 32) != 0) ? 1 : 0;
|
||||
// Feature 4: above 4GB (in real 64-bit address space)
|
||||
passed += (val > 0x100000000ULL) ? 1 : 0;
|
||||
// Feature 5: user-mode address range (not kernel)
|
||||
passed += (val < 0xFFFF800000000000ULL) ? 1 : 0;
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
inline FeatureResult countPtrFeatures32(uint32_t val) {
|
||||
int passed = 0, checked = 3;
|
||||
// Feature 1: non-zero and not sentinel
|
||||
passed += (val != 0 && val != 0xFFFFFFFF) ? 1 : 0;
|
||||
// Feature 2: aligned to 4
|
||||
passed += ((val & 3) == 0) ? 1 : 0;
|
||||
// Feature 3: above null guard pages (>= 64KB)
|
||||
passed += (val >= 0x10000) ? 1 : 0;
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── String feature checker ──
|
||||
|
||||
inline FeatureResult countStringFeatures(const uint8_t* data, int len) {
|
||||
if (len < 2) return {0, 4};
|
||||
int printable = 0, letters = 0, consecutive = 0, maxConsec = 0;
|
||||
for (int i = 0; i < len; ++i) {
|
||||
if (isPrintable(data[i])) {
|
||||
printable++;
|
||||
consecutive++;
|
||||
maxConsec = std::max(maxConsec, consecutive);
|
||||
if ((data[i] >= 'A' && data[i] <= 'Z') || (data[i] >= 'a' && data[i] <= 'z'))
|
||||
letters++;
|
||||
} else {
|
||||
consecutive = 0;
|
||||
}
|
||||
}
|
||||
double ratio = (double)printable / len;
|
||||
int passed = 0, checked = 4;
|
||||
passed += (maxConsec >= 4) ? 1 : 0;
|
||||
passed += (ratio > 0.75) ? 1 : 0;
|
||||
passed += (letters >= 1) ? 1 : 0;
|
||||
passed += (ratio > 0.90) ? 1 : 0;
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Int16 feature checker ──
|
||||
|
||||
inline FeatureResult countInt16Features(uint16_t val,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
int passed = 0, checked = 2;
|
||||
int16_t sv = (int16_t)val;
|
||||
passed += (val != 0) ? 1 : 0;
|
||||
passed += (sv >= -16384 && sv <= 16384) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 2;
|
||||
uint16_t minV = loadU16(minP), maxV = loadU16(maxP);
|
||||
passed += (minV <= 4096 && maxV <= 4096) ? 1 : 0;
|
||||
passed += (minV != maxV) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Score from feature result ──
|
||||
|
||||
inline int featureScore(FeatureResult r) {
|
||||
if (r.checked == 0) return 0;
|
||||
return (r.passed * 100) / r.checked;
|
||||
}
|
||||
|
||||
inline int strengthFromScore(int score) {
|
||||
if (score >= 75) return 3;
|
||||
if (score >= 50) return 2;
|
||||
if (score >= 25) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Candidate accumulator ──
|
||||
|
||||
struct Candidate {
|
||||
QVector<NodeKind> kinds;
|
||||
int score;
|
||||
};
|
||||
|
||||
inline void addCandidate(QVector<Candidate>& out, NodeKind k, int score) {
|
||||
if (score >= 25) out.push_back(Candidate{{k}, score});
|
||||
}
|
||||
|
||||
inline void addSplitCandidate(QVector<Candidate>& out, NodeKind k, int count, int score) {
|
||||
if (score >= 25) {
|
||||
QVector<NodeKind> kinds(count, k);
|
||||
out.push_back(Candidate{std::move(kinds), score});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Try whole-width interpretations ──
|
||||
|
||||
inline void tryWhole8(const uint8_t* data, const InferHints& h, QVector<Candidate>& out) {
|
||||
uint64_t u64 = loadU64(data);
|
||||
|
||||
// Pointer64
|
||||
if (h.ptrSize == 8)
|
||||
addCandidate(out, NodeKind::Pointer64, featureScore(countPtrFeatures64(u64)));
|
||||
|
||||
// Double — rare in RE work; require strong evidence
|
||||
{
|
||||
double d; std::memcpy(&d, data, 8);
|
||||
double ad = std::fabs(d);
|
||||
uint64_t mantissa = u64 & 0x000FFFFFFFFFFFFFull;
|
||||
// Hard reject: outside plausible range [1e-6, 1e7] (matches float checker)
|
||||
bool inRange = (d == 0.0 || (ad >= 1e-6 && ad <= 1e7));
|
||||
// Hard reject: lower 32 zero with non-zero mantissa (two 32-bit fields)
|
||||
bool splitField = ((u64 & 0xFFFFFFFF) == 0 && mantissa != 0);
|
||||
if (inRange && !splitField) {
|
||||
uint64_t exp = (u64 >> 52) & 0x7FF;
|
||||
int passed = 0, checked = 4;
|
||||
// Feature 1: finite
|
||||
passed += std::isfinite(d) ? 1 : 0;
|
||||
// Feature 2: non-denormal
|
||||
passed += (exp > 0 || (u64 & 0x7FFFFFFFFFFFFFFFull) == 0) ? 1 : 0;
|
||||
// Feature 3: has fractional part or is a small special value
|
||||
double ip; double frac = std::fabs(std::modf(d, &ip));
|
||||
passed += (frac > 0.001 || ad <= 1.0) ? 1 : 0;
|
||||
// Feature 4: not a large exact integer (likely reinterpreted binary data)
|
||||
passed += !(ad > 1000.0 && frac < 0.001) ? 1 : 0;
|
||||
addCandidate(out, NodeKind::Double, featureScore({passed, checked}));
|
||||
}
|
||||
}
|
||||
|
||||
// UTF8
|
||||
addCandidate(out, NodeKind::UTF8, featureScore(countStringFeatures(data, 8)));
|
||||
|
||||
// UInt64 / Int64 — only meaningful when value exceeds 32-bit range
|
||||
if ((u64 >> 32) != 0) {
|
||||
int passed = 0, checked = 3;
|
||||
// Feature 1: non-zero (always true after guard)
|
||||
passed += 1;
|
||||
// Feature 2: reasonable magnitude (below kernel range)
|
||||
passed += (u64 < 0x0000FFFFFFFFFFFFULL) ? 1 : 0;
|
||||
// Feature 3: monotonic or page-aligned
|
||||
passed += (h.monotonic || (u64 & 0xFFF) == 0) ? 1 : 0;
|
||||
addCandidate(out, NodeKind::UInt64, featureScore({passed, checked}));
|
||||
}
|
||||
}
|
||||
|
||||
inline void tryWhole4(const uint8_t* data, const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h, QVector<Candidate>& out) {
|
||||
uint32_t u32 = loadU32(data);
|
||||
|
||||
// Float
|
||||
addCandidate(out, NodeKind::Float, featureScore(countFloatFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// Int32
|
||||
addCandidate(out, NodeKind::Int32, featureScore(countIntFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// UInt32
|
||||
addCandidate(out, NodeKind::UInt32, featureScore(countIntFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// Flags (only if sparse bits)
|
||||
addCandidate(out, NodeKind::UInt32, featureScore(countFlagFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// Pointer32
|
||||
if (h.ptrSize == 4)
|
||||
addCandidate(out, NodeKind::Pointer32, featureScore(countPtrFeatures32(u32)));
|
||||
}
|
||||
|
||||
inline void tryWhole2(const uint8_t* data, const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h, QVector<Candidate>& out) {
|
||||
uint16_t u16 = loadU16(data);
|
||||
int scoreI = featureScore(countInt16Features(u16, minP, maxP, h));
|
||||
addCandidate(out, NodeKind::Int16, scoreI);
|
||||
addCandidate(out, NodeKind::UInt16, scoreI);
|
||||
}
|
||||
|
||||
inline void tryWhole1(const uint8_t* data, QVector<Candidate>& out) {
|
||||
uint8_t v = data[0];
|
||||
int score = (v == 0 || v == 1) ? 50 : 25;
|
||||
addCandidate(out, NodeKind::UInt8, score);
|
||||
}
|
||||
|
||||
// ── Try uniform splits ──
|
||||
|
||||
inline void trySplitUniform(const uint8_t* data, int len,
|
||||
const InferHints& h,
|
||||
QVector<Candidate>& out) {
|
||||
|
||||
// 8 → 2×4
|
||||
if (len == 8) {
|
||||
const uint8_t* minA = h.minObserved;
|
||||
const uint8_t* minB = h.minObserved ? h.minObserved + 4 : nullptr;
|
||||
const uint8_t* maxA = h.maxObserved;
|
||||
const uint8_t* maxB = h.maxObserved ? h.maxObserved + 4 : nullptr;
|
||||
bool zA = allZero(data, 4), zB = allZero(data + 4, 4);
|
||||
|
||||
// Float×2: both halves must be good floats and at least one non-zero
|
||||
if (!zA || !zB) {
|
||||
uint32_t bitsA = loadU32(data), bitsB = loadU32(data + 4);
|
||||
bool fA = zA || isGoodFloat(bitsA);
|
||||
bool fB = zB || isGoodFloat(bitsB);
|
||||
if (fA && fB) {
|
||||
auto rA = zA ? FeatureResult{2, 4} : countFloatFeatures(bitsA, minA, maxA, h);
|
||||
auto rB = zB ? FeatureResult{2, 4} : countFloatFeatures(bitsB, minB, maxB, h);
|
||||
int score = std::min(featureScore(rA), featureScore(rB));
|
||||
addSplitCandidate(out, NodeKind::Float, 2, score);
|
||||
}
|
||||
}
|
||||
|
||||
// Int32×2: both halves, at least one non-zero
|
||||
if (!zA || !zB) {
|
||||
auto rA = zA ? FeatureResult{1, 3} : countIntFeatures(loadU32(data), minA, maxA, h);
|
||||
auto rB = zB ? FeatureResult{1, 3} : countIntFeatures(loadU32(data + 4), minB, maxB, h);
|
||||
int score = std::min(featureScore(rA), featureScore(rB));
|
||||
addSplitCandidate(out, NodeKind::Int32, 2, score);
|
||||
}
|
||||
|
||||
// UInt32×2
|
||||
if (!zA || !zB) {
|
||||
auto rA = zA ? FeatureResult{1, 3} : countIntFeatures(loadU32(data), minA, maxA, h);
|
||||
auto rB = zB ? FeatureResult{1, 3} : countIntFeatures(loadU32(data + 4), minB, maxB, h);
|
||||
int score = std::min(featureScore(rA), featureScore(rB));
|
||||
addSplitCandidate(out, NodeKind::UInt32, 2, score);
|
||||
}
|
||||
}
|
||||
|
||||
// 8 → 4×2 or 4 → 2×2
|
||||
int halfLen = len / 2;
|
||||
if (halfLen == 2) {
|
||||
int minScore = 100;
|
||||
int count = len / 2;
|
||||
bool anyNonZero = false;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t* part = data + i * 2;
|
||||
if (!allZero(part, 2)) anyNonZero = true;
|
||||
const uint8_t* mp = h.minObserved ? h.minObserved + i * 2 : nullptr;
|
||||
const uint8_t* xp = h.maxObserved ? h.maxObserved + i * 2 : nullptr;
|
||||
int s = featureScore(countInt16Features(loadU16(part), mp, xp, h));
|
||||
minScore = std::min(minScore, s);
|
||||
}
|
||||
if (anyNonZero) {
|
||||
addSplitCandidate(out, NodeKind::Int16, count, minScore);
|
||||
addSplitCandidate(out, NodeKind::UInt16, count, minScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Prune and rank ──
|
||||
|
||||
inline QVector<TypeSuggestion> pruneAndRank(QVector<Candidate>& cands, int maxResults) {
|
||||
// Sort descending by score
|
||||
std::sort(cands.begin(), cands.end(), [](const Candidate& a, const Candidate& b) {
|
||||
return a.score > b.score;
|
||||
});
|
||||
|
||||
// Dedup: keep highest-scoring per unique kinds vector
|
||||
QVector<Candidate> deduped;
|
||||
for (const auto& c : cands) {
|
||||
bool dup = false;
|
||||
for (const auto& d : deduped) {
|
||||
if (d.kinds == c.kinds) { dup = true; break; }
|
||||
}
|
||||
if (!dup) deduped.append(c);
|
||||
}
|
||||
|
||||
// Dominance: if top >= 1.5× second, keep only top
|
||||
if (deduped.size() >= 2 && deduped[0].score >= deduped[1].score * 3 / 2)
|
||||
deduped.resize(1);
|
||||
else if (deduped.size() > maxResults)
|
||||
deduped.resize(maxResults);
|
||||
|
||||
QVector<TypeSuggestion> result;
|
||||
result.reserve(deduped.size());
|
||||
for (const auto& c : deduped) {
|
||||
int str = strengthFromScore(c.score);
|
||||
if (str > 0)
|
||||
result.push_back(TypeSuggestion{c.kinds, c.score, str});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
inline QVector<TypeSuggestion> inferTypes(
|
||||
const uint8_t* data, int len,
|
||||
const InferHints& hints,
|
||||
int maxResults)
|
||||
{
|
||||
using namespace detail;
|
||||
|
||||
if (!data || len <= 0) return {};
|
||||
if (allZero(data, len)) return {}; // NULL → skip entirely
|
||||
|
||||
QVector<Candidate> cands;
|
||||
cands.reserve(12);
|
||||
|
||||
// Whole-width candidates
|
||||
if (len >= 8) tryWhole8(data, hints, cands);
|
||||
if (len == 4) tryWhole4(data, hints.minObserved, hints.maxObserved, hints, cands);
|
||||
if (len == 2) tryWhole2(data, hints.minObserved, hints.maxObserved, hints, cands);
|
||||
if (len == 1) tryWhole1(data, cands);
|
||||
|
||||
// Uniform splits (compete directly with whole-width candidates)
|
||||
if (len >= 4)
|
||||
trySplitUniform(data, len, hints, cands);
|
||||
|
||||
return pruneAndRank(cands, maxResults);
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
@@ -57,51 +57,73 @@ TypeSpec parseTypeSpec(const QString& text) {
|
||||
}
|
||||
|
||||
// ── Fuzzy scorer: subsequence match with word-boundary bonuses ──
|
||||
// Hot path — uses stack arrays and pre-lowered QChars to avoid heap allocs.
|
||||
|
||||
static constexpr int kMaxFuzzyLen = 64;
|
||||
|
||||
static int fuzzyScore(const QString& pattern, const QString& text,
|
||||
QVector<int>* outPositions = nullptr) {
|
||||
int pLen = pattern.size(), tLen = text.size();
|
||||
if (pLen == 0) return 1;
|
||||
if (pLen > tLen) return 0;
|
||||
if (pLen > kMaxFuzzyLen || tLen > 256) {
|
||||
// Fallback: prefix match only for very long names
|
||||
if (text.startsWith(pattern, Qt::CaseInsensitive)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Quick subsequence reject
|
||||
// Pre-compute lowercase chars on the stack
|
||||
QChar pLow[kMaxFuzzyLen];
|
||||
for (int i = 0; i < pLen; i++) pLow[i] = pattern[i].toLower();
|
||||
QChar tLow[256];
|
||||
for (int i = 0; i < tLen; i++) tLow[i] = text[i].toLower();
|
||||
|
||||
// Quick subsequence reject using pre-lowered arrays
|
||||
{ int pi = 0;
|
||||
for (int ti = 0; ti < tLen && pi < pLen; ti++)
|
||||
if (pattern[pi].toLower() == text[ti].toLower()) pi++;
|
||||
if (pLow[pi] == tLow[ti]) pi++;
|
||||
if (pi < pLen) return 0;
|
||||
}
|
||||
|
||||
// Recursive best-match (bounded: max 4 branches per pattern char)
|
||||
QVector<int> bestPos;
|
||||
// Stack arrays instead of QVector to avoid heap allocation
|
||||
int bestPos[kMaxFuzzyLen];
|
||||
int curPos[kMaxFuzzyLen];
|
||||
int best = 0;
|
||||
int bestLen = 0;
|
||||
|
||||
auto solve = [&](auto& self, int pi, int ti, QVector<int>& cur, int score) -> void {
|
||||
auto solve = [&](auto& self, int pi, int ti, int curLen, int score) -> void {
|
||||
if (pi == pLen) {
|
||||
if (score > best) { best = score; bestPos = cur; }
|
||||
if (score > best) {
|
||||
best = score;
|
||||
bestLen = curLen;
|
||||
memcpy(bestPos, curPos, curLen * sizeof(int));
|
||||
}
|
||||
return;
|
||||
}
|
||||
int maxTi = tLen - (pLen - pi);
|
||||
int branches = 0;
|
||||
for (int i = ti; i <= maxTi && branches < 4; i++) {
|
||||
if (pattern[pi].toLower() != text[i].toLower()) continue;
|
||||
if (pLow[pi] != tLow[i]) continue;
|
||||
int bonus = 1;
|
||||
if (i == 0) bonus = 10;
|
||||
else if (text[i - 1] == '_' || text[i - 1] == ' ') bonus = 8;
|
||||
else if (text[i].isUpper() && text[i - 1].isLower()) bonus = 8;
|
||||
if (!cur.isEmpty() && i == cur.last() + 1) bonus += 5;
|
||||
cur.append(i);
|
||||
self(self, pi + 1, i + 1, cur, score + bonus);
|
||||
cur.removeLast();
|
||||
if (curLen > 0 && i == curPos[curLen - 1] + 1) bonus += 5;
|
||||
curPos[curLen] = i;
|
||||
self(self, pi + 1, i + 1, curLen + 1, score + bonus);
|
||||
branches++;
|
||||
}
|
||||
};
|
||||
|
||||
QVector<int> cur;
|
||||
solve(solve, 0, 0, cur, 0);
|
||||
solve(solve, 0, 0, 0, 0);
|
||||
if (best > 0) {
|
||||
best += qMax(0, 20 - (tLen - pLen)); // tightness bonus
|
||||
if (pLen == tLen) best += 20; // exact match bonus
|
||||
if (outPositions) *outPositions = bestPos;
|
||||
if (outPositions) {
|
||||
outPositions->resize(bestLen);
|
||||
memcpy(outPositions->data(), bestPos, bestLen * sizeof(int));
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -113,7 +135,7 @@ public:
|
||||
explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr)
|
||||
: QStyledItemDelegate(parent), m_popup(popup) {}
|
||||
|
||||
void setFont(const QFont& f) { m_font = f; }
|
||||
void setFont(const QFont& f) { m_font = f; updateCachedSizeHint(); }
|
||||
void setLoading(bool v) { m_isLoading = v; }
|
||||
void setFilteredTypes(const QVector<TypeEntry>* filtered) {
|
||||
m_filtered = filtered;
|
||||
@@ -287,13 +309,13 @@ public:
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
|
||||
const QModelIndex& index) const override {
|
||||
const QModelIndex& /*index*/) const override {
|
||||
return m_cachedSizeHint;
|
||||
}
|
||||
|
||||
void updateCachedSizeHint() {
|
||||
QFontMetrics fm(m_font);
|
||||
int row = index.row();
|
||||
bool isSection = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||
&& (*m_filtered)[row].entryKind == TypeEntry::Section);
|
||||
int h = isSection ? fm.height() + 2 : fm.height() + 8;
|
||||
return QSize(200, h);
|
||||
m_cachedSizeHint = QSize(200, fm.height() + 8);
|
||||
}
|
||||
|
||||
bool helpEvent(QHelpEvent* event, QAbstractItemView* view,
|
||||
@@ -304,8 +326,9 @@ public:
|
||||
if (row >= 0 && row < m_filtered->size()) {
|
||||
const auto& e = (*m_filtered)[row];
|
||||
if (e.entryKind == TypeEntry::Composite && !e.fieldSummary.isEmpty()) {
|
||||
QString tip = QStringLiteral("%1 (%2 B, %3 fields)\n")
|
||||
.arg(e.displayName).arg(e.sizeBytes).arg(e.fieldCount);
|
||||
QString tip = QStringLiteral("%1 (0x%2 bytes, %3 fields)\n")
|
||||
.arg(e.displayName, QString::number(e.sizeBytes, 16).toUpper())
|
||||
.arg(e.fieldCount);
|
||||
tip += e.fieldSummary.join(QChar('\n'));
|
||||
if (e.fieldCount > e.fieldSummary.size())
|
||||
tip += QStringLiteral("\n...");
|
||||
@@ -322,6 +345,7 @@ public:
|
||||
private:
|
||||
TypeSelectorPopup* m_popup = nullptr;
|
||||
QFont m_font;
|
||||
QSize m_cachedSizeHint{200, 20};
|
||||
bool m_isLoading = false;
|
||||
const QVector<TypeEntry>* m_filtered = nullptr;
|
||||
const QVector<QVector<int>>* m_matchPositions = nullptr;
|
||||
@@ -415,9 +439,9 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
return btn;
|
||||
};
|
||||
|
||||
m_chipPrim = makeChip(QStringLiteral("P"));
|
||||
m_chipTypes = makeChip(QStringLiteral("T"));
|
||||
m_chipEnums = makeChip(QStringLiteral("E"));
|
||||
m_chipPrim = makeChip(QStringLiteral("Built-in"));
|
||||
m_chipTypes = makeChip(QStringLiteral("Types"));
|
||||
m_chipEnums = makeChip(QStringLiteral("Enum"));
|
||||
m_chipPrim->setAccessibleName(QStringLiteral("Show primitives"));
|
||||
m_chipTypes->setAccessibleName(QStringLiteral("Show composites"));
|
||||
m_chipEnums->setAccessibleName(QStringLiteral("Show enums"));
|
||||
@@ -448,6 +472,9 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||
m_listView->setAccessibleName(QStringLiteral("Type list"));
|
||||
m_listView->setUniformItemSizes(true);
|
||||
m_listView->setLayoutMode(QListView::Batched);
|
||||
m_listView->setBatchSize(50);
|
||||
m_listView->installEventFilter(this);
|
||||
|
||||
auto* delegate = new TypeSelectorDelegate(this, m_listView);
|
||||
@@ -714,6 +741,7 @@ void TypeSelectorPopup::applyTheme(const Theme& theme) {
|
||||
m_titleLabel->setPalette(pal);
|
||||
m_filterEdit->setPalette(pal);
|
||||
m_listView->setPalette(pal);
|
||||
m_listView->viewport()->setPalette(pal);
|
||||
m_arrayCountEdit->setPalette(pal);
|
||||
|
||||
// Esc button (snapped to corner)
|
||||
@@ -826,6 +854,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
if (delegate) delegate->setLoading(false);
|
||||
|
||||
m_allTypes = types;
|
||||
// Cache max display name length for popup width calculation
|
||||
m_cachedMaxNameLen = 0;
|
||||
for (const auto& t : m_allTypes) {
|
||||
if (t.entryKind != TypeEntry::Section)
|
||||
m_cachedMaxNameLen = qMax(m_cachedMaxNameLen, (int)t.displayName.size());
|
||||
}
|
||||
if (current) {
|
||||
m_currentEntry = *current;
|
||||
m_hasCurrent = true;
|
||||
@@ -858,13 +892,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
|
||||
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||
QFontMetrics fm(m_font);
|
||||
int maxTextW = fm.horizontalAdvance(QStringLiteral("Choose element type "));
|
||||
for (const auto& t : m_allTypes) {
|
||||
int iconColW = fm.height() + 4;
|
||||
int w = iconColW + fm.horizontalAdvance(t.displayName) + 16;
|
||||
if (w > maxTextW) maxTextW = w;
|
||||
}
|
||||
int popupW = qBound(480, maxTextW + 24, 560);
|
||||
constexpr int kMaxPopupW = 560;
|
||||
// Estimate max width from cached max name length (avoids iterating all types)
|
||||
int iconColW = fm.height() + 4;
|
||||
int estMaxW = iconColW + fm.horizontalAdvance(QChar('W')) * m_cachedMaxNameLen + 16;
|
||||
int maxTextW = qMax(fm.horizontalAdvance(QStringLiteral("Choose element type ")), estMaxW);
|
||||
int popupW = qBound(480, maxTextW + 24, kMaxPopupW);
|
||||
int rowH = fm.height() + 8;
|
||||
int headerH = rowH * 2 + 10; // filter + chips + separator
|
||||
int footerH = rowH + 6; // separator + action row
|
||||
@@ -968,18 +1001,27 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
|
||||
auto makeLabel = [](const TypeEntry& e) {
|
||||
QString label = e.displayName;
|
||||
if (e.sizeBytes > 0) label += QStringLiteral(" - %1").arg(e.sizeBytes);
|
||||
if (e.sizeBytes > 0) label += QStringLiteral(" - 0x%1 bytes").arg(QString::number(e.sizeBytes, 16).toUpper());
|
||||
return label;
|
||||
};
|
||||
|
||||
int primCount = 0, typeCount = 0, enumCount = 0;
|
||||
const int totalTypes = m_allTypes.size();
|
||||
|
||||
// Pre-reserve to avoid realloc churn
|
||||
m_filteredTypes.reserve(totalTypes);
|
||||
m_matchPositions.reserve(totalTypes);
|
||||
displayStrings.reserve(totalTypes);
|
||||
|
||||
if (!filterBase.isEmpty()) {
|
||||
// ── Fuzzy search: flat ranked list, no section headers ──
|
||||
struct Scored { TypeEntry entry; int score; QVector<int> pos; };
|
||||
// Use index + score to avoid deep-copying TypeEntry structs
|
||||
struct Scored { int idx; int score; QVector<int> pos; };
|
||||
QVector<Scored> scored;
|
||||
scored.reserve(totalTypes);
|
||||
|
||||
for (const auto& t : m_allTypes) {
|
||||
for (int i = 0; i < totalTypes; i++) {
|
||||
const auto& t = m_allTypes[i];
|
||||
if (t.entryKind == TypeEntry::Section) continue;
|
||||
QVector<int> pos;
|
||||
int sc = fuzzyScore(filterBase, t.displayName, &pos);
|
||||
@@ -988,15 +1030,15 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
else if (t.category == TypeEntry::CatEnum) enumCount++;
|
||||
else typeCount++;
|
||||
if (catAllowed(t))
|
||||
scored.append({t, sc, pos});
|
||||
scored.push_back(Scored{i, sc, std::move(pos)});
|
||||
}
|
||||
std::sort(scored.begin(), scored.end(),
|
||||
[](const Scored& a, const Scored& b) { return a.score > b.score; });
|
||||
|
||||
for (const auto& s : scored) {
|
||||
m_filteredTypes.append(s.entry);
|
||||
m_filteredTypes.append(m_allTypes[s.idx]);
|
||||
m_matchPositions.append(s.pos);
|
||||
displayStrings << makeLabel(s.entry);
|
||||
displayStrings << makeLabel(m_allTypes[s.idx]);
|
||||
}
|
||||
} else {
|
||||
// ── No filter: grouped sections, alphabetical ──
|
||||
@@ -1080,9 +1122,9 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
auto updateChipLabel = [](QToolButton* btn, const QString& abbrev, int count) {
|
||||
btn->setText(QStringLiteral("%1 (%2)").arg(abbrev).arg(count));
|
||||
};
|
||||
if (m_chipPrim) updateChipLabel(m_chipPrim, QStringLiteral("P"), primCount);
|
||||
if (m_chipTypes) updateChipLabel(m_chipTypes, QStringLiteral("T"), typeCount);
|
||||
if (m_chipEnums) updateChipLabel(m_chipEnums, QStringLiteral("E"), enumCount);
|
||||
if (m_chipPrim) updateChipLabel(m_chipPrim, QStringLiteral("Built-in"), primCount);
|
||||
if (m_chipTypes) updateChipLabel(m_chipTypes, QStringLiteral("Types"), typeCount);
|
||||
if (m_chipEnums) updateChipLabel(m_chipEnums, QStringLiteral("Enum"), enumCount);
|
||||
|
||||
if (m_statusLabel)
|
||||
m_statusLabel->setText(QStringLiteral("%1 results").arg(resultCount));
|
||||
|
||||
@@ -120,6 +120,7 @@ private:
|
||||
int m_pointerSize = 8;
|
||||
bool m_loading = false;
|
||||
QFont m_font;
|
||||
int m_cachedMaxNameLen = 0; // longest displayName length (chars)
|
||||
|
||||
void applyFilter(const QString& text);
|
||||
void updateModifierPreview();
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QPainter>
|
||||
#include <QApplication>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
@@ -10,25 +15,91 @@ namespace rcx {
|
||||
struct TabInfo {
|
||||
const NodeTree* tree;
|
||||
QString name;
|
||||
void* subPtr; // QMdiSubWindow* as void*
|
||||
void* subPtr; // QDockWidget* as void*
|
||||
};
|
||||
|
||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
||||
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
|
||||
// Helper: is a Hex padding node
|
||||
inline bool isHexPad(NodeKind k) {
|
||||
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
||||
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
||||
}
|
||||
|
||||
// Build child rows for a struct item.
|
||||
inline void buildStructChildren(QStandardItem* item,
|
||||
const NodeTree* tree, uint64_t structId,
|
||||
void* subPtr) {
|
||||
item->removeRows(0, item->rowCount());
|
||||
|
||||
QVector<int> members = tree->childrenOf(structId);
|
||||
std::sort(members.begin(), members.end(), [&](int a, int b) {
|
||||
return tree->nodes[a].offset < tree->nodes[b].offset;
|
||||
});
|
||||
|
||||
auto memberTypeName = [](const Node& m) -> QString {
|
||||
if (m.kind == NodeKind::Struct) {
|
||||
return m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
|
||||
: m.structTypeName;
|
||||
}
|
||||
return QString::fromLatin1(kindToString(m.kind));
|
||||
};
|
||||
|
||||
for (int mi : members) {
|
||||
if (mi < 0 || mi >= tree->nodes.size()) continue;
|
||||
const Node& m = tree->nodes[mi];
|
||||
if (isHexPad(m.kind)) continue;
|
||||
QString childDisplay = QStringLiteral("%1 %2")
|
||||
.arg(memberTypeName(m), m.name);
|
||||
auto* childItem = new QStandardItem(childDisplay);
|
||||
childItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
|
||||
item->appendRow(childItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to build display string for a type entry.
|
||||
inline QString typeDisplayString(const Node* node, const NodeTree* tree) {
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
if (node->resolvedClassKeyword() == QStringLiteral("enum")) {
|
||||
return QStringLiteral("%1 \u2014 %2")
|
||||
.arg(nameOf(node),
|
||||
QString::number(node->enumMembers.size()));
|
||||
}
|
||||
QVector<int> members = tree->childrenOf(node->id);
|
||||
int vc = 0;
|
||||
for (int mi : members)
|
||||
if (!isHexPad(tree->nodes[mi].kind)) ++vc;
|
||||
return QStringLiteral("%1 \u2014 %2")
|
||||
.arg(nameOf(node), QString::number(vc));
|
||||
}
|
||||
|
||||
// Build a new item for a type entry.
|
||||
inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
|
||||
void* subPtr) {
|
||||
static const QIcon enumIcon(":/vsicons/symbol-enum.svg");
|
||||
static const QIcon structIcon(":/vsicons/symbol-structure.svg");
|
||||
bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum");
|
||||
auto* item = new QStandardItem(
|
||||
isEnum ? enumIcon : structIcon,
|
||||
typeDisplayString(node, tree));
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1);
|
||||
item->setData(isEnum, Qt::UserRole + 2);
|
||||
|
||||
if (!isEnum)
|
||||
buildStructChildren(item, tree, node->id, subPtr);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Full rebuild — used by benchmarks and first build.
|
||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
const QVector<TabInfo>& tabs,
|
||||
const QSet<uint64_t>& pinnedIds = {}) {
|
||||
model->clear();
|
||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||
|
||||
// Single "Project" root with folder icon
|
||||
void* firstSub = tabs.isEmpty() ? nullptr : tabs[0].subPtr;
|
||||
auto* projectItem = new QStandardItem(QIcon(":/vsicons/folder.svg"),
|
||||
QStringLiteral("Project"));
|
||||
projectItem->setData(QVariant::fromValue(firstSub), Qt::UserRole);
|
||||
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
||||
|
||||
// Collect all top-level structs/enums across all tabs
|
||||
struct Entry { const Node* node; void* subPtr; const NodeTree* tree; };
|
||||
QVector<Entry> types, enums;
|
||||
for (const auto& tab : tabs) {
|
||||
@@ -37,84 +108,251 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
const Node& n = tab.tree->nodes[idx];
|
||||
if (n.kind != NodeKind::Struct) continue;
|
||||
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||
enums.append({&n, tab.subPtr, tab.tree});
|
||||
enums.push_back(Entry{&n, tab.subPtr, tab.tree});
|
||||
else
|
||||
types.append({&n, tab.subPtr, tab.tree});
|
||||
types.push_back(Entry{&n, tab.subPtr, tab.tree});
|
||||
}
|
||||
}
|
||||
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
auto cmpName = [&](const Entry& a, const Entry& b) {
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpName);
|
||||
std::sort(enums.begin(), enums.end(), cmpName);
|
||||
|
||||
// Helper: type display string for a member node
|
||||
auto memberTypeName = [](const Node& m) -> QString {
|
||||
if (m.kind == NodeKind::Struct) {
|
||||
QString stn = m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
|
||||
: m.structTypeName;
|
||||
return stn;
|
||||
}
|
||||
return QString::fromLatin1(kindToString(m.kind));
|
||||
};
|
||||
|
||||
// Helper: is a Hex padding node
|
||||
auto isHexPad = [](NodeKind k) {
|
||||
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
||||
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
||||
};
|
||||
|
||||
// Pinned items at the very top, then structs, then enums
|
||||
QVector<Entry> pinned;
|
||||
QVector<Entry> unpinnedTypes, unpinnedEnums;
|
||||
for (const auto& e : types) {
|
||||
QVector<int> members = e.tree->childrenOf(e.node->id);
|
||||
|
||||
// Count non-hex members for display
|
||||
int visibleCount = 0;
|
||||
for (int mi : members)
|
||||
if (!isHexPad(e.tree->nodes[mi].kind)) ++visibleCount;
|
||||
|
||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
||||
QString::number(visibleCount));
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-structure.svg"), display);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||
|
||||
// Add child rows sorted by offset (skip Hex padding)
|
||||
std::sort(members.begin(), members.end(), [&](int a, int b) {
|
||||
return e.tree->nodes[a].offset < e.tree->nodes[b].offset;
|
||||
});
|
||||
for (int mi : members) {
|
||||
const Node& m = e.tree->nodes[mi];
|
||||
if (isHexPad(m.kind)) continue;
|
||||
QString childDisplay = QStringLiteral("%1 %2")
|
||||
.arg(memberTypeName(m), m.name);
|
||||
auto* childItem = new QStandardItem(childDisplay);
|
||||
childItem->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
|
||||
item->appendRow(childItem);
|
||||
}
|
||||
|
||||
projectItem->appendRow(item);
|
||||
if (pinnedIds.contains(e.node->id)) pinned.append(e);
|
||||
else unpinnedTypes.append(e);
|
||||
}
|
||||
|
||||
for (const auto& e : enums) {
|
||||
int count = e.node->enumMembers.size();
|
||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
||||
QString::number(count));
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-enum.svg"), display);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
if (pinnedIds.contains(e.node->id)) pinned.append(e);
|
||||
else unpinnedEnums.append(e);
|
||||
}
|
||||
|
||||
model->appendRow(projectItem);
|
||||
for (const auto& e : pinned)
|
||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||
for (const auto& e : unpinnedTypes)
|
||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||
for (const auto& e : unpinnedEnums)
|
||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||
}
|
||||
|
||||
// Incremental sync — preserves tree expansion/scroll state.
|
||||
inline void syncProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs,
|
||||
const QSet<uint64_t>& pinnedIds = {}) {
|
||||
// First call — full build
|
||||
if (model->rowCount() == 0 && !tabs.isEmpty()) {
|
||||
buildProjectExplorer(model, tabs, pinnedIds);
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect desired entries
|
||||
struct Entry { uint64_t id; const Node* node; void* subPtr; const NodeTree* tree; bool isEnum; };
|
||||
QVector<Entry> desired;
|
||||
for (const auto& tab : tabs) {
|
||||
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||
for (int idx : topLevel) {
|
||||
const Node& n = tab.tree->nodes[idx];
|
||||
if (n.kind != NodeKind::Struct) continue;
|
||||
bool ie = n.resolvedClassKeyword() == QStringLiteral("enum");
|
||||
desired.push_back(Entry{n.id, &n, tab.subPtr, tab.tree, ie});
|
||||
}
|
||||
}
|
||||
|
||||
QHash<uint64_t, int> desiredMap;
|
||||
desiredMap.reserve(desired.size());
|
||||
for (int i = 0; i < desired.size(); ++i)
|
||||
desiredMap[desired[i].id] = i;
|
||||
|
||||
// Remove stale items (backwards)
|
||||
for (int i = model->rowCount() - 1; i >= 0; --i) {
|
||||
auto* item = model->item(i);
|
||||
if (!item) continue;
|
||||
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
|
||||
if (!desiredMap.contains(id))
|
||||
model->removeRow(i);
|
||||
}
|
||||
|
||||
// Update existing items
|
||||
QSet<uint64_t> existing;
|
||||
for (int i = 0; i < model->rowCount(); ++i) {
|
||||
auto* item = model->item(i);
|
||||
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
|
||||
existing.insert(id);
|
||||
auto dit = desiredMap.find(id);
|
||||
if (dit == desiredMap.end()) continue;
|
||||
const Entry& e = desired[*dit];
|
||||
|
||||
QString display = typeDisplayString(e.node, e.tree);
|
||||
if (item->text() != display)
|
||||
item->setText(display);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
|
||||
// Refresh children only when count changed (avoids destroying expansion state)
|
||||
if (!e.isEnum) {
|
||||
QVector<int> members = e.tree->childrenOf(id);
|
||||
int visCount = 0;
|
||||
for (int mi : members)
|
||||
if (!isHexPad(e.tree->nodes[mi].kind)) ++visCount;
|
||||
if (item->rowCount() != visCount)
|
||||
buildStructChildren(item, e.tree, id, e.subPtr);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new items
|
||||
for (const auto& e : desired) {
|
||||
if (existing.contains(e.id)) continue;
|
||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||
}
|
||||
|
||||
if (model->horizontalHeaderItem(0) == nullptr)
|
||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||
}
|
||||
|
||||
// ── Custom delegate for rich workspace tree rendering ──
|
||||
|
||||
class WorkspaceDelegate : public QStyledItemDelegate {
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
|
||||
void setThemeColors(const Theme& t) {
|
||||
m_text = t.text;
|
||||
m_textDim = t.textDim;
|
||||
m_textMuted = t.textMuted;
|
||||
m_syntaxType = t.syntaxType;
|
||||
m_hover = t.hover;
|
||||
m_selected = t.selected;
|
||||
m_accent = t.borderFocused; // left accent bar
|
||||
m_bg = t.background;
|
||||
m_badgeBg = t.backgroundAlt;
|
||||
m_badgeText = t.textDim;
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override {
|
||||
QSize s = QStyledItemDelegate::sizeHint(option, index);
|
||||
int pad = index.parent().isValid() ? 6 : 10;
|
||||
s.setHeight(option.fontMetrics.height() + pad);
|
||||
return s;
|
||||
}
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override {
|
||||
painter->save();
|
||||
|
||||
QStyleOptionViewItem opt = option;
|
||||
initStyleOption(&opt, index);
|
||||
opt.text.clear();
|
||||
opt.icon = QIcon(); // we draw icon manually
|
||||
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
|
||||
|
||||
// Custom background for selection/hover
|
||||
if (opt.state & QStyle::State_Selected) {
|
||||
painter->fillRect(opt.rect, m_selected);
|
||||
// Left accent bar
|
||||
painter->fillRect(QRect(opt.rect.x(), opt.rect.y(), 1, opt.rect.height()), m_accent);
|
||||
} else if (opt.state & QStyle::State_MouseOver) {
|
||||
painter->fillRect(opt.rect, m_hover);
|
||||
}
|
||||
|
||||
bool isChild = index.parent().isValid();
|
||||
QString fullText = index.data(Qt::DisplayRole).toString();
|
||||
QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
|
||||
|
||||
// Letter badge (S/E for top-level, F for children)
|
||||
{
|
||||
QChar letter = 'F';
|
||||
if (!isChild) {
|
||||
bool isEnum = index.data(Qt::UserRole + 2).toBool();
|
||||
letter = isEnum ? 'E' : 'S';
|
||||
}
|
||||
int sz = opt.fontMetrics.height();
|
||||
int y = textRect.y() + (textRect.height() - sz) / 2;
|
||||
QRect badge(textRect.x(), y, sz, sz);
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(m_badgeBg);
|
||||
painter->drawRoundedRect(badge, 3, 3);
|
||||
QColor letterCol = m_badgeText;
|
||||
if (!isChild && !index.data(Qt::UserRole + 3).toBool())
|
||||
letterCol.setAlpha(100);
|
||||
painter->setPen(letterCol);
|
||||
QFont bf = opt.font;
|
||||
bf.setBold(true);
|
||||
painter->setFont(bf);
|
||||
painter->drawText(badge, Qt::AlignCenter, letter);
|
||||
painter->setRenderHint(QPainter::Antialiasing, false);
|
||||
textRect.setLeft(textRect.left() + sz + 4);
|
||||
}
|
||||
|
||||
painter->setFont(opt.font);
|
||||
|
||||
if (!isChild) {
|
||||
// Top-level: "StructName — 3" → name left, count pill right
|
||||
int dashPos = fullText.indexOf(QChar(0x2014));
|
||||
QString name = (dashPos > 1) ? fullText.left(dashPos - 1) : fullText;
|
||||
QString count = (dashPos > 1) ? fullText.mid(dashPos + 2).trimmed() : QString();
|
||||
|
||||
bool pinned = index.data(Qt::UserRole + 4).toBool();
|
||||
|
||||
// Reserve right side for pin icon + count pill
|
||||
int rightEdge = textRect.right();
|
||||
if (!count.isEmpty()) {
|
||||
int cw = opt.fontMetrics.horizontalAdvance(count) + 10;
|
||||
int ch = opt.fontMetrics.height();
|
||||
int cy = textRect.y() + (textRect.height() - ch) / 2;
|
||||
QRect pill(rightEdge - cw, cy, cw, ch);
|
||||
rightEdge = pill.left() - 2;
|
||||
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(m_badgeBg);
|
||||
painter->drawRect(pill);
|
||||
painter->setPen(m_textMuted);
|
||||
painter->drawText(pill, Qt::AlignCenter, count);
|
||||
}
|
||||
if (pinned) {
|
||||
static const QIcon pinIcon(":/vsicons/pin.svg");
|
||||
int isz = opt.fontMetrics.height() - 2;
|
||||
int iy = textRect.y() + (textRect.height() - isz) / 2;
|
||||
QRect pinRect(rightEdge - isz, iy, isz, isz);
|
||||
pinIcon.paint(painter, pinRect);
|
||||
rightEdge = pinRect.left() - 2;
|
||||
}
|
||||
|
||||
// Draw name clipped before right-side elements
|
||||
if (rightEdge > textRect.left() + 4) {
|
||||
QRect nameRect = textRect;
|
||||
nameRect.setRight(rightEdge);
|
||||
QString elided = opt.fontMetrics.elidedText(name, Qt::ElideRight, nameRect.width());
|
||||
painter->setPen(m_text);
|
||||
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
|
||||
}
|
||||
} else {
|
||||
// Child: "TypeName fieldName"
|
||||
int spacePos = fullText.indexOf(' ');
|
||||
if (spacePos > 0) {
|
||||
QString typeName = fullText.left(spacePos);
|
||||
QString fieldName = fullText.mid(spacePos);
|
||||
|
||||
painter->setPen(m_syntaxType);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, typeName);
|
||||
int typeW = opt.fontMetrics.horizontalAdvance(typeName);
|
||||
|
||||
QRect fieldRect = textRect;
|
||||
fieldRect.setLeft(textRect.left() + typeW);
|
||||
painter->setPen(m_textDim);
|
||||
painter->drawText(fieldRect, Qt::AlignLeft | Qt::AlignVCenter, fieldName);
|
||||
} else {
|
||||
painter->setPen(m_textDim);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
|
||||
}
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
private:
|
||||
QColor m_text, m_textDim, m_textMuted, m_syntaxType;
|
||||
QColor m_hover, m_selected, m_accent, m_bg;
|
||||
QColor m_badgeBg, m_badgeText;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -65,6 +65,7 @@ private:
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void benchCompose();
|
||||
void benchComposeLarge();
|
||||
void benchApplyDocument();
|
||||
void benchHoverHighlight();
|
||||
void benchSelectionOverlay();
|
||||
@@ -112,6 +113,36 @@ void BenchLargeClass::benchCompose()
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchComposeLarge()
|
||||
{
|
||||
// Build a 2000-field tree to stress-test compose at scale
|
||||
NodeTree bigTree = buildLargeTree(2000);
|
||||
QByteArray buf(0x40000, '\0');
|
||||
for (int i = 0; i < buf.size(); ++i) buf[i] = (char)(i & 0xFF);
|
||||
BufferProvider bigProv(buf, QStringLiteral("bench_large"));
|
||||
|
||||
// Warmup
|
||||
{ ComposeResult w = rcx::compose(bigTree, bigProv); Q_UNUSED(w); }
|
||||
|
||||
const int ITERS = 50;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
ComposeResult r = rcx::compose(bigTree, bigProv);
|
||||
Q_UNUSED(r);
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Compose Benchmark (2000 fields) ===";
|
||||
qDebug() << " Tree:" << bigTree.nodes.size() << "nodes";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-compose:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchApplyDocument()
|
||||
{
|
||||
RcxEditor editor;
|
||||
|
||||
282
tests/bench_project.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* bench_project — benchmark project lifecycle operations:
|
||||
* - New class creation
|
||||
* - Loading large .rcx files (WinSDK, Vergilius)
|
||||
* - Workspace model building
|
||||
* - Workspace search filtering
|
||||
* - JSON parsing vs model building breakdown
|
||||
*/
|
||||
#include <QtTest/QtTest>
|
||||
#include <QElapsedTimer>
|
||||
#include <QJsonDocument>
|
||||
#include <QStandardItemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include "core.h"
|
||||
#include "controller.h"
|
||||
#include "workspace_model.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class BenchProject : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void benchNewClass();
|
||||
void benchLoadVergilius();
|
||||
void benchLoadWinSDK();
|
||||
void benchJsonParse();
|
||||
void benchNodeTreeFromJson();
|
||||
void benchBuildWorkspaceModel();
|
||||
void benchWorkspaceSearch();
|
||||
};
|
||||
|
||||
static QString findExample(const QString& name) {
|
||||
// Try relative to executable, then common build layout
|
||||
QStringList candidates = {
|
||||
QCoreApplication::applicationDirPath() + "/examples/" + name,
|
||||
QCoreApplication::applicationDirPath() + "/../src/examples/" + name,
|
||||
QStringLiteral("src/examples/") + name,
|
||||
QStringLiteral("../src/examples/") + name,
|
||||
};
|
||||
for (const auto& c : candidates)
|
||||
if (QFileInfo::exists(c)) return c;
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── New class (just the core operations, no UI) ──
|
||||
|
||||
void BenchProject::benchNewClass()
|
||||
{
|
||||
const int ITERS = 1000;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("NewClass");
|
||||
root.structTypeName = QStringLiteral("NewClass");
|
||||
root.classKeyword = QStringLiteral("class");
|
||||
tree.addNode(root);
|
||||
// Add 8 hex64 padding fields (what buildEmptyStruct does)
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Hex64;
|
||||
pad.name = QString();
|
||||
pad.parentId = rootId;
|
||||
pad.offset = j * 8;
|
||||
tree.addNode(pad);
|
||||
}
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== New Class (core tree build) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-new:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
// ── Load .rcx files ──
|
||||
|
||||
static bool loadRcx(const QString& path, NodeTree& tree) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) return false;
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||
tree = NodeTree::fromJson(jdoc.object());
|
||||
return !tree.nodes.isEmpty();
|
||||
}
|
||||
|
||||
void BenchProject::benchLoadVergilius()
|
||||
{
|
||||
QString path = findExample("Vergilius_25H2.rcx");
|
||||
if (path.isEmpty()) { QSKIP("Vergilius_25H2.rcx not found"); return; }
|
||||
|
||||
const int ITERS = 5;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
QVERIFY(loadRcx(path, tree));
|
||||
if (i == 0)
|
||||
qDebug() << " Nodes:" << tree.nodes.size();
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Load Vergilius_25H2.rcx ===";
|
||||
qDebug() << " File:" << QFileInfo(path).size() / 1024 << "KB";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-load:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
void BenchProject::benchLoadWinSDK()
|
||||
{
|
||||
QString path = findExample("WinSDK.rcx");
|
||||
if (path.isEmpty()) { QSKIP("WinSDK.rcx not found"); return; }
|
||||
|
||||
const int ITERS = 5;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
QVERIFY(loadRcx(path, tree));
|
||||
if (i == 0)
|
||||
qDebug() << " Nodes:" << tree.nodes.size();
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Load WinSDK.rcx ===";
|
||||
qDebug() << " File:" << QFileInfo(path).size() / 1024 << "KB";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-load:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
// ── Breakdown: JSON parse vs NodeTree build ──
|
||||
|
||||
void BenchProject::benchJsonParse()
|
||||
{
|
||||
QString path = findExample("Vergilius_25H2.rcx");
|
||||
if (path.isEmpty()) path = findExample("WinSDK.rcx");
|
||||
if (path.isEmpty()) { QSKIP("No large .rcx found"); return; }
|
||||
|
||||
QFile f(path);
|
||||
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
const int ITERS = 5;
|
||||
|
||||
// Phase 1: raw JSON parse
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
QJsonDocument jdoc;
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
jdoc = QJsonDocument::fromJson(data);
|
||||
qint64 jsonMs = timer.elapsed();
|
||||
|
||||
// Phase 2: NodeTree::fromJson
|
||||
QJsonObject root = jdoc.object();
|
||||
timer.start();
|
||||
NodeTree tree;
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
tree = NodeTree::fromJson(root);
|
||||
qint64 treeMs = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== JSON Parse Breakdown ===" << QFileInfo(path).fileName();
|
||||
qDebug() << " File:" << data.size() / 1024 << "KB," << tree.nodes.size() << "nodes";
|
||||
qDebug() << " JSON parse:" << (double)jsonMs / ITERS << "ms/iter";
|
||||
qDebug() << " NodeTree build:" << (double)treeMs / ITERS << "ms/iter";
|
||||
qDebug() << " Total per-load:" << (double)(jsonMs + treeMs) / ITERS << "ms";
|
||||
}
|
||||
|
||||
void BenchProject::benchNodeTreeFromJson()
|
||||
{
|
||||
// Already covered by benchJsonParse breakdown
|
||||
QVERIFY(true);
|
||||
}
|
||||
|
||||
// ── Workspace model building ──
|
||||
|
||||
void BenchProject::benchBuildWorkspaceModel()
|
||||
{
|
||||
// Load both large examples if available
|
||||
QVector<NodeTree> trees;
|
||||
for (const auto& name : {QStringLiteral("Vergilius_25H2.rcx"), QStringLiteral("WinSDK.rcx")}) {
|
||||
QString path = findExample(name);
|
||||
if (path.isEmpty()) continue;
|
||||
NodeTree t;
|
||||
if (loadRcx(path, t)) trees.append(std::move(t));
|
||||
}
|
||||
if (trees.isEmpty()) { QSKIP("No .rcx examples found"); return; }
|
||||
|
||||
// Build TabInfo array
|
||||
QVector<TabInfo> tabs;
|
||||
for (const auto& t : trees)
|
||||
tabs.push_back(TabInfo{ &t, QStringLiteral("test"), nullptr });
|
||||
|
||||
QStandardItemModel model;
|
||||
const int ITERS = 20;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
buildProjectExplorer(&model, tabs);
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
// Count items
|
||||
int topLevel = model.rowCount();
|
||||
int totalChildren = 0;
|
||||
for (int i = 0; i < topLevel; ++i)
|
||||
totalChildren += model.item(i)->rowCount();
|
||||
|
||||
int totalNodes = 0;
|
||||
for (const auto& t : trees) totalNodes += t.nodes.size();
|
||||
fprintf(stderr, "\n=== Build Workspace Model ===\n");
|
||||
fprintf(stderr, " Trees: %d total nodes: %d\n", (int)trees.size(), totalNodes);
|
||||
fprintf(stderr, " Top-level items: %d child items: %d\n", topLevel, totalChildren);
|
||||
fprintf(stderr, " Iterations: %d\n", ITERS);
|
||||
fprintf(stderr, " Total: %lld ms\n", (long long)elapsed);
|
||||
fprintf(stderr, " Per-build: %.1f ms\n", (double)elapsed / ITERS);
|
||||
}
|
||||
|
||||
// ── Workspace search filtering ──
|
||||
|
||||
void BenchProject::benchWorkspaceSearch()
|
||||
{
|
||||
QVector<NodeTree> trees;
|
||||
for (const auto& name : {QStringLiteral("Vergilius_25H2.rcx"), QStringLiteral("WinSDK.rcx")}) {
|
||||
QString path = findExample(name);
|
||||
if (path.isEmpty()) continue;
|
||||
NodeTree t;
|
||||
if (loadRcx(path, t)) trees.append(std::move(t));
|
||||
}
|
||||
if (trees.isEmpty()) { QSKIP("No .rcx examples found"); return; }
|
||||
|
||||
QVector<TabInfo> tabs;
|
||||
for (const auto& t : trees)
|
||||
tabs.push_back(TabInfo{ &t, QStringLiteral("test"), nullptr });
|
||||
|
||||
QStandardItemModel model;
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QSortFilterProxyModel proxy;
|
||||
proxy.setSourceModel(&model);
|
||||
proxy.setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxy.setRecursiveFilteringEnabled(true);
|
||||
|
||||
const QStringList queries = {
|
||||
"EPROCESS", "KTHREAD", "LIST_ENTRY", "HAL", "DMA",
|
||||
"xyz_no_match", "a", "Dispatch"
|
||||
};
|
||||
|
||||
const int ITERS = 50;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
for (const auto& q : queries)
|
||||
proxy.setFilterFixedString(q);
|
||||
proxy.setFilterFixedString(QString()); // clear
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
int totalOps = ITERS * (queries.size() + 1);
|
||||
fprintf(stderr, "\n=== Workspace Search Filter ===\n");
|
||||
fprintf(stderr, " Model rows: %d queries: %d\n", model.rowCount(), (int)queries.size());
|
||||
fprintf(stderr, " Iterations: %d total filter ops: %d\n", ITERS, totalOps);
|
||||
fprintf(stderr, " Total: %lld ms\n", (long long)elapsed);
|
||||
fprintf(stderr, " Per-filter: %.2f ms\n", (double)elapsed / totalOps);
|
||||
}
|
||||
|
||||
QTEST_MAIN(BenchProject)
|
||||
#include "bench_project.moc"
|
||||
222
tests/grab_tabs.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QApplication>
|
||||
#include <QMainWindow>
|
||||
#include <QDockWidget>
|
||||
#include <QTabBar>
|
||||
#include <QTextEdit>
|
||||
#include <QPixmap>
|
||||
#include <QToolButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QProxyStyle>
|
||||
#include <QStyleOptionTab>
|
||||
#include <QSettings>
|
||||
#include <QPainter>
|
||||
#include "../src/themes/thememanager.h"
|
||||
|
||||
// Minimal replica of the real app's MenuBarStyle for dock tab painting
|
||||
class TestTabStyle : public QProxyStyle {
|
||||
public:
|
||||
using QProxyStyle::QProxyStyle;
|
||||
|
||||
QSize sizeFromContents(ContentsType type, const QStyleOption* opt,
|
||||
const QSize& sz, const QWidget* w) const override {
|
||||
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
|
||||
if (type == CT_TabBarTab) {
|
||||
if (auto* tabBar = qobject_cast<const QTabBar*>(w)) {
|
||||
if (tabBar->parent() && qobject_cast<const QMainWindow*>(tabBar->parent()))
|
||||
s.setHeight(28);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void drawControl(ControlElement element, const QStyleOption* opt,
|
||||
QPainter* p, const QWidget* w) const override {
|
||||
// Tab shape — background, accent line, borders
|
||||
if (element == CE_TabBarTabShape) {
|
||||
if (auto* tab = qstyleoption_cast<const QStyleOptionTab*>(opt)) {
|
||||
auto* tabBar = qobject_cast<const QTabBar*>(w);
|
||||
if (tabBar && tabBar->parent() && qobject_cast<QMainWindow*>(tabBar->parent())) {
|
||||
bool selected = tab->state & State_Selected;
|
||||
bool hovered = tab->state & State_MouseOver;
|
||||
QColor bg = tab->palette.color(QPalette::Window);
|
||||
if (hovered && !selected)
|
||||
bg = tab->palette.color(QPalette::Mid);
|
||||
p->fillRect(tab->rect, bg);
|
||||
if (selected)
|
||||
p->fillRect(QRect(tab->rect.left(), tab->rect.top(),
|
||||
tab->rect.width(), 2),
|
||||
tab->palette.color(QPalette::Link));
|
||||
p->setPen(tab->palette.color(QPalette::Dark));
|
||||
p->drawLine(tab->rect.bottomLeft(), tab->rect.bottomRight());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tab label — middle-elide long names, editor font
|
||||
if (element == CE_TabBarTabLabel) {
|
||||
if (auto* tab = qstyleoption_cast<const QStyleOptionTab*>(opt)) {
|
||||
auto* tabBar = qobject_cast<const QTabBar*>(w);
|
||||
if (tabBar && tabBar->parent() && qobject_cast<QMainWindow*>(tabBar->parent())) {
|
||||
int tabIdx = -1;
|
||||
for (int i = 0; i < tabBar->count(); ++i) {
|
||||
if (tabBar->tabRect(i).contains(tab->rect.center())) { tabIdx = i; break; }
|
||||
}
|
||||
int btnWidth = 0;
|
||||
if (tabIdx >= 0) {
|
||||
auto* btn = tabBar->tabButton(tabIdx, QTabBar::RightSide);
|
||||
if (btn) btnWidth = btn->sizeHint().width() + 4;
|
||||
}
|
||||
QRect textRect = tab->rect.adjusted(8, 0, -(8 + btnWidth), 0);
|
||||
QFont f("JetBrains Mono", 10);
|
||||
f.setFixedPitch(true);
|
||||
p->setFont(f);
|
||||
QFontMetrics fm(f);
|
||||
QString text = (tabIdx >= 0) ? tabBar->tabText(tabIdx) : tab->text;
|
||||
int maxW = textRect.width();
|
||||
if (fm.horizontalAdvance(text) > maxW) {
|
||||
int ellW = fm.horizontalAdvance(QStringLiteral("\u2026"));
|
||||
int avail = maxW - ellW;
|
||||
if (avail > 0) {
|
||||
int half = avail / 2;
|
||||
QString left, right;
|
||||
for (int i = 0; i < text.size(); ++i)
|
||||
if (fm.horizontalAdvance(text.left(i+1)) > half) { left = text.left(i); break; }
|
||||
if (left.isEmpty()) left = text.left(1);
|
||||
for (int i = text.size()-1; i >= 0; --i)
|
||||
if (fm.horizontalAdvance(text.mid(i)) > half) { right = text.mid(i+1); break; }
|
||||
if (right.isEmpty()) right = text.right(1);
|
||||
text = left + QStringLiteral("\u2026") + right;
|
||||
} else {
|
||||
text = QStringLiteral("\u2026");
|
||||
}
|
||||
}
|
||||
bool selected = tab->state & QStyle::State_Selected;
|
||||
p->setPen(selected ? tab->palette.color(QPalette::Text)
|
||||
: tab->palette.color(QPalette::WindowText));
|
||||
p->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
QProxyStyle::drawControl(element, opt, p, w);
|
||||
}
|
||||
};
|
||||
|
||||
class TabBtns : public QWidget {
|
||||
public:
|
||||
explicit TabBtns(const QColor& hover, QWidget* parent = nullptr) : QWidget(parent) {
|
||||
auto* hl = new QHBoxLayout(this);
|
||||
hl->setContentsMargins(2, 0, 0, 0);
|
||||
hl->setSpacing(0);
|
||||
QString style = QStringLiteral(
|
||||
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
|
||||
"QToolButton:hover { background: %1; }").arg(hover.name());
|
||||
auto* pin = new QToolButton(this);
|
||||
pin->setFixedSize(16, 16);
|
||||
pin->setAutoRaise(true);
|
||||
pin->setIcon(QIcon(":/vsicons/pin.svg"));
|
||||
pin->setIconSize(QSize(12, 12));
|
||||
pin->setStyleSheet(style);
|
||||
hl->addWidget(pin);
|
||||
auto* close = new QToolButton(this);
|
||||
close->setFixedSize(16, 16);
|
||||
close->setAutoRaise(true);
|
||||
close->setIcon(QIcon(":/vsicons/close.svg"));
|
||||
close->setIconSize(QSize(12, 12));
|
||||
close->setStyleSheet(style);
|
||||
hl->addWidget(close);
|
||||
}
|
||||
};
|
||||
|
||||
class GrabTabs : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void grab() {
|
||||
const auto& t = rcx::ThemeManager::instance().current();
|
||||
|
||||
// Install custom style (no stylesheet — all painting via style)
|
||||
QApplication::setStyle(new TestTabStyle("Fusion"));
|
||||
|
||||
// Apply dark palette globally
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, t.background);
|
||||
pal.setColor(QPalette::WindowText, t.textDim);
|
||||
pal.setColor(QPalette::Base, t.background);
|
||||
pal.setColor(QPalette::Text, t.text);
|
||||
pal.setColor(QPalette::Mid, t.hover);
|
||||
pal.setColor(QPalette::Dark, t.border);
|
||||
pal.setColor(QPalette::Link, t.indHoverSpan);
|
||||
QApplication::setPalette(pal);
|
||||
|
||||
auto* win = new QMainWindow;
|
||||
win->resize(700, 500);
|
||||
win->setDockNestingEnabled(true);
|
||||
win->setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North);
|
||||
|
||||
auto* central = new QWidget(win);
|
||||
central->setMaximumSize(0, 0);
|
||||
central->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||||
win->setCentralWidget(central);
|
||||
win->setStyleSheet(QStringLiteral(
|
||||
"QMainWindow::separator { width: 0px; height: 0px; background: transparent; }"));
|
||||
|
||||
QStringList names = {
|
||||
"shader_color_helper.hpp",
|
||||
"shader_crypt.cpp",
|
||||
"EPROCESS (class)",
|
||||
"very_long_struct_name_that_should_elide.h"
|
||||
};
|
||||
|
||||
QVector<QDockWidget*> docks;
|
||||
for (const auto& name : names) {
|
||||
auto* dock = new QDockWidget(name, win);
|
||||
dock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
auto* emptyTitle = new QWidget(dock);
|
||||
emptyTitle->setFixedHeight(0);
|
||||
dock->setTitleBarWidget(emptyTitle);
|
||||
dock->setWidget(new QTextEdit(dock));
|
||||
if (!docks.isEmpty())
|
||||
win->tabifyDockWidget(docks.last(), dock);
|
||||
else
|
||||
win->addDockWidget(Qt::TopDockWidgetArea, dock);
|
||||
docks.append(dock);
|
||||
}
|
||||
// Select first tab
|
||||
docks.first()->raise();
|
||||
|
||||
win->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(win));
|
||||
QApplication::processEvents();
|
||||
|
||||
// No stylesheet on dock tab bars — painting handled by TestTabStyle
|
||||
for (auto* tabBar : win->findChildren<QTabBar*>()) {
|
||||
if (tabBar->parent() != win) continue;
|
||||
tabBar->setStyleSheet(QString());
|
||||
tabBar->setElideMode(Qt::ElideNone);
|
||||
tabBar->setExpanding(false);
|
||||
|
||||
QPalette tp = tabBar->palette();
|
||||
tp.setColor(QPalette::WindowText, t.textDim);
|
||||
tp.setColor(QPalette::Text, t.text);
|
||||
tp.setColor(QPalette::Window, t.background);
|
||||
tp.setColor(QPalette::Mid, t.hover);
|
||||
tp.setColor(QPalette::Dark, t.border);
|
||||
tp.setColor(QPalette::Link, t.indHoverSpan);
|
||||
tabBar->setPalette(tp);
|
||||
|
||||
for (int i = 0; i < tabBar->count(); ++i)
|
||||
tabBar->setTabButton(i, QTabBar::RightSide, new TabBtns(t.hover, tabBar));
|
||||
}
|
||||
QApplication::processEvents();
|
||||
QApplication::processEvents();
|
||||
|
||||
QPixmap shot = win->grab(QRect(0, 0, win->width(), 50));
|
||||
shot.save(QStringLiteral("tab_screenshot.png"));
|
||||
qDebug() << "Saved" << shot.size();
|
||||
delete win;
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(GrabTabs)
|
||||
#include "grab_tabs.moc"
|
||||
@@ -382,6 +382,30 @@ private slots:
|
||||
QCOMPARE(r.value, 0x140000000ULL);
|
||||
}
|
||||
|
||||
// -- Bare module.dll identifier --
|
||||
|
||||
void bareModuleDll() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "client.dll");
|
||||
return *ok ? 0x7FF600000000ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("client.dll + 0xFF", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x7FF6000000FFULL);
|
||||
}
|
||||
|
||||
void bareModuleExe() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "cs2.exe");
|
||||
return *ok ? 0x140000000ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("cs2.exe + 0xDE", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x1400000DEULL);
|
||||
}
|
||||
|
||||
// -- Validate with new syntax --
|
||||
|
||||
void validateIdentifier() {
|
||||
|
||||