mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
101 Commits
v2027.02.1
...
snapshot-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a19789a9d | ||
|
|
62a68bef80 | ||
|
|
4941f860b6 | ||
|
|
c45d51d736 | ||
|
|
5b46065403 | ||
|
|
4706f7b782 | ||
|
|
fe9bfafa3b | ||
|
|
ff928df685 | ||
|
|
d6e3c182fc | ||
|
|
078a6028f0 | ||
|
|
d7a6e1862e | ||
|
|
1ddf47a754 | ||
|
|
1a885a8b1d | ||
|
|
67218d3e48 | ||
|
|
f651edd740 | ||
|
|
25aaace382 | ||
|
|
b5ddb042b8 | ||
|
|
e900dea836 | ||
|
|
b647a334bc | ||
|
|
fc390bc1f7 | ||
|
|
7efe740ec1 | ||
|
|
48409d1d38 | ||
|
|
df1435d9b7 | ||
|
|
5e11ff5496 | ||
|
|
22842d9801 | ||
|
|
50acde60cb | ||
|
|
1d7d384b93 | ||
|
|
3a76b03c85 | ||
|
|
ac94855d6c | ||
|
|
d65b6c5a29 | ||
|
|
d45ee9e4c9 | ||
|
|
31115014a5 | ||
|
|
8e88d588be | ||
|
|
b089e20d36 | ||
|
|
5fa1dd0ab4 | ||
|
|
3b1fe7ff35 | ||
|
|
4595b366e3 | ||
|
|
33d7dc74cb | ||
|
|
e118231bb1 | ||
|
|
0cfd7ad87a | ||
|
|
2d3ce63b54 | ||
|
|
0e087fa3a4 | ||
|
|
c7afe363f3 | ||
|
|
2a44d2ac57 | ||
|
|
d989e2a947 | ||
|
|
7678da033d | ||
|
|
acc3ebf5db | ||
|
|
26217f5de8 | ||
|
|
fa0d9a377b | ||
|
|
b1d3e52204 | ||
|
|
1cccd320b0 | ||
|
|
5b6e0473cb | ||
|
|
57d55456a8 | ||
|
|
bb466516ba | ||
|
|
444ba34fa3 | ||
|
|
91633169a0 | ||
|
|
f041761b62 | ||
|
|
1c3b4af045 | ||
|
|
5ae9ca0979 | ||
|
|
e064646c02 | ||
|
|
c6c56ffaee | ||
|
|
aba8e5cac9 | ||
|
|
3a5d03fae0 | ||
|
|
df79da54e3 | ||
|
|
e3ff4dfe71 | ||
|
|
735e4ea9f7 | ||
|
|
d937d2f42e | ||
|
|
3685530287 | ||
|
|
9e90f66ca0 | ||
|
|
f53fa84a15 | ||
|
|
13e28e8791 | ||
|
|
079b3121ce | ||
|
|
5e40349768 | ||
|
|
8dd6110ec6 | ||
|
|
eb27fc7988 | ||
|
|
85994d68b9 | ||
|
|
55dc5d5875 | ||
|
|
3a92336132 | ||
|
|
f9b33f2ba7 | ||
|
|
f2dab07870 | ||
|
|
9d22a5ed69 | ||
|
|
193ab81ecf | ||
|
|
aa0840b332 | ||
|
|
f3631f17ff | ||
|
|
42e9bde7ba | ||
|
|
07fedf0ae8 | ||
|
|
2e02a01495 | ||
|
|
71bc51cbab | ||
|
|
60a97ab81b | ||
|
|
bb00e75019 | ||
|
|
c038c59e34 | ||
|
|
862f76b984 | ||
|
|
818285a76e | ||
|
|
ef5e2ebdb9 | ||
|
|
75fedd2222 | ||
|
|
389745e501 | ||
|
|
1473a58742 | ||
|
|
4192a4dad3 | ||
|
|
4c6bb9564f | ||
|
|
0ef9841f90 | ||
|
|
0a8244dad4 |
171
.github/workflows/build.yml
vendored
Normal file
171
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- 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'
|
||||
cache: true
|
||||
|
||||
- name: Configure
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
gcc --version
|
||||
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF \
|
||||
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
cmake --build build
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Package release zip
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
mkdir -p release
|
||||
cp build/Reclass.exe release/
|
||||
cp build/ReclassMcpBridge.exe release/
|
||||
cp build/*.dll release/ 2>/dev/null || true
|
||||
cp -r build/platforms release/ 2>/dev/null || true
|
||||
cp -r build/styles release/ 2>/dev/null || true
|
||||
cp -r build/imageformats release/ 2>/dev/null || true
|
||||
cp -r build/iconengines release/ 2>/dev/null || true
|
||||
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 -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
|
||||
cd release && 7z a ../Reclass-win64-qt6.zip *
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Reclass-win64-qt6
|
||||
path: Reclass-win64-qt6.zip
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Create AppImage
|
||||
run: |
|
||||
# Download linuxdeploy and Qt plugin
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
|
||||
# Build AppDir structure
|
||||
mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps
|
||||
cp build/Reclass AppDir/usr/bin/
|
||||
cp build/ReclassMcpBridge AppDir/usr/bin/
|
||||
cp -r build/themes AppDir/usr/bin/ 2>/dev/null || true
|
||||
cp -r build/examples AppDir/usr/bin/ 2>/dev/null || true
|
||||
mkdir -p AppDir/usr/bin/Plugins
|
||||
cp build/Plugins/*.so AppDir/usr/bin/Plugins/ 2>/dev/null || true
|
||||
cp src/icons/class.png AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png
|
||||
|
||||
# Create AppImage with Qt libs bundled
|
||||
# install-qt-action adds Qt bin to PATH; find qmake there
|
||||
QMAKE_BIN=$(which qmake 2>/dev/null || which qmake6 2>/dev/null || find "$RUNNER_WORKSPACE" -name qmake -path "*/bin/*" | head -1)
|
||||
echo "Found qmake at: $QMAKE_BIN"
|
||||
export QMAKE="$QMAKE_BIN"
|
||||
QT_ROOT=$(dirname "$(dirname "$QMAKE_BIN")")
|
||||
export LD_LIBRARY_PATH="$QT_ROOT/lib:$LD_LIBRARY_PATH"
|
||||
export EXTRA_QT_PLUGINS="svg;iconengines"
|
||||
./linuxdeploy-x86_64.AppImage --appdir AppDir \
|
||||
--desktop-file deploy/Reclass.desktop \
|
||||
--icon-file AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png \
|
||||
--plugin qt \
|
||||
--output appimage
|
||||
# Rename to final name
|
||||
ls Reclass-*.AppImage
|
||||
mv Reclass-*.AppImage Reclass-linux64-qt6.AppImage
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [windows, linux]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Get date tag
|
||||
id: date
|
||||
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||
name: Snapshot ${{ steps.date.outputs.tag }}
|
||||
body: |
|
||||
Automated snapshot from main branch.
|
||||
Commit: ${{ github.sha }}
|
||||
prerelease: false
|
||||
files: |
|
||||
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
||||
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ build/
|
||||
*.suo
|
||||
.vs/
|
||||
CMakeUserPresets.json
|
||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||
.cache
|
||||
|
||||
266
CMakeLists.txt
266
CMakeLists.txt
@@ -1,8 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Reclass VERSION 0.1 LANGUAGES CXX)
|
||||
project(Reclass VERSION 0.1 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
@@ -30,6 +31,15 @@ endif()
|
||||
|
||||
find_package(QScintilla REQUIRED)
|
||||
|
||||
# RawPDB — direct PDB file reader (no DIA SDK / msdia140.dll dependency)
|
||||
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)
|
||||
if(WIN32)
|
||||
target_link_libraries(raw_pdb PRIVATE rpcrt4)
|
||||
endif()
|
||||
|
||||
add_executable(Reclass
|
||||
src/main.cpp
|
||||
src/editor.h
|
||||
@@ -59,15 +69,33 @@ add_executable(Reclass
|
||||
src/themes/thememanager.cpp
|
||||
src/themes/themeeditor.h
|
||||
src/themes/themeeditor.cpp
|
||||
src/imports/import_reclass_xml.h
|
||||
src/imports/import_reclass_xml.cpp
|
||||
src/imports/import_source.h
|
||||
src/imports/import_source.cpp
|
||||
src/imports/export_reclass_xml.h
|
||||
src/imports/export_reclass_xml.cpp
|
||||
src/imports/import_pdb.h
|
||||
src/imports/import_pdb.cpp
|
||||
src/imports/import_pdb_dialog.h
|
||||
src/imports/import_pdb_dialog.cpp
|
||||
src/mainwindow.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/addressparser.h
|
||||
src/addressparser.cpp
|
||||
src/disasm.h
|
||||
src/disasm.cpp
|
||||
third_party/fadec/decode.c
|
||||
third_party/fadec/format.c
|
||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||
)
|
||||
|
||||
target_include_directories(Reclass PRIVATE src)
|
||||
target_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||
|
||||
target_link_libraries(Reclass PRIVATE
|
||||
${QT}::Widgets
|
||||
@@ -79,7 +107,7 @@ target_link_libraries(Reclass PRIVATE
|
||||
${_QT_WINEXTRAS}
|
||||
)
|
||||
if(WIN32)
|
||||
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi)
|
||||
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
|
||||
endif()
|
||||
|
||||
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
||||
@@ -93,14 +121,16 @@ foreach(_tf ${_theme_files})
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
# Copy example .rcx files to build directory
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
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()
|
||||
|
||||
include(deploy)
|
||||
|
||||
add_custom_target(screenshot ALL
|
||||
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
||||
DEPENDS Reclass deploy
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
COMMENT "Capturing UI screenshot with class open..."
|
||||
)
|
||||
|
||||
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
|
||||
file(WRITE ${_combine_script} "
|
||||
@@ -117,7 +147,7 @@ foreach(_f
|
||||
\"${CMAKE_SOURCE_DIR}/src/generator.cpp\"
|
||||
\"${CMAKE_SOURCE_DIR}/src/main.cpp\")
|
||||
file(READ \${_f} _content)
|
||||
file(APPEND \${_out} \${_content})
|
||||
file(APPEND \${_out} \"\${_content}\")
|
||||
file(APPEND \${_out} \"\\n\")
|
||||
endforeach()
|
||||
message(STATUS \"Combined sources -> \${_out}\")
|
||||
@@ -134,29 +164,26 @@ if(BUILD_TESTING)
|
||||
find_package(${QT} REQUIRED COMPONENTS Test)
|
||||
enable_testing()
|
||||
|
||||
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
|
||||
# Disasm/Fadec sources needed by any test that links editor.cpp
|
||||
set(DISASM_SRCS src/disasm.cpp third_party/fadec/decode.c third_party/fadec/format.c)
|
||||
|
||||
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
|
||||
|
||||
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_core PRIVATE src)
|
||||
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_core COMMAND test_core)
|
||||
|
||||
add_executable(test_format tests/test_format.cpp src/format.cpp)
|
||||
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)
|
||||
add_test(NAME test_format COMMAND test_format)
|
||||
|
||||
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp)
|
||||
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_compose PRIVATE src)
|
||||
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_compose COMMAND test_compose)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_editor PRIVATE src)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_provider tests/test_provider.cpp)
|
||||
target_include_directories(test_provider PRIVATE src)
|
||||
target_link_libraries(test_provider PRIVATE ${QT}::Core ${QT}::Test)
|
||||
@@ -167,12 +194,68 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_command_row COMMAND test_command_row)
|
||||
|
||||
add_executable(test_generator tests/test_generator.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_generator PRIVATE src)
|
||||
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_generator COMMAND test_generator)
|
||||
|
||||
add_executable(test_import_xml tests/test_import_xml.cpp
|
||||
src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_import_xml PRIVATE src)
|
||||
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_import_xml COMMAND test_import_xml)
|
||||
|
||||
add_executable(test_import_source tests/test_import_source.cpp
|
||||
src/imports/import_source.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_import_source PRIVATE src)
|
||||
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_import_source COMMAND test_import_source)
|
||||
|
||||
add_executable(test_export_xml tests/test_export_xml.cpp
|
||||
src/imports/export_reclass_xml.cpp src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_export_xml PRIVATE src)
|
||||
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_export_xml COMMAND test_export_xml)
|
||||
|
||||
add_executable(test_disasm tests/test_disasm.cpp
|
||||
src/disasm.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
third_party/fadec/decode.c third_party/fadec/format.c)
|
||||
target_include_directories(test_disasm PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_disasm COMMAND test_disasm)
|
||||
|
||||
add_executable(test_addressparser tests/test_addressparser.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_addressparser PRIVATE src)
|
||||
target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_addressparser COMMAND test_addressparser)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_import_pdb tests/test_import_pdb.cpp
|
||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_import_pdb PRIVATE src)
|
||||
target_link_libraries(test_import_pdb PRIVATE
|
||||
${QT}::Core ${QT}::Test raw_pdb)
|
||||
add_test(NAME test_import_pdb COMMAND test_import_pdb)
|
||||
|
||||
add_executable(bench_import_pdb tests/bench_import_pdb.cpp
|
||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(bench_import_pdb PRIVATE src)
|
||||
target_link_libraries(bench_import_pdb PRIVATE
|
||||
${QT}::Core ${QT}::Test raw_pdb)
|
||||
add_test(NAME bench_import_pdb COMMAND bench_import_pdb)
|
||||
endif()
|
||||
|
||||
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
|
||||
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/controller.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)
|
||||
target_include_directories(test_controller PRIVATE src)
|
||||
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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -182,11 +265,11 @@ if(BUILD_TESTING)
|
||||
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/controller.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)
|
||||
target_include_directories(test_validation PRIVATE src)
|
||||
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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -195,18 +278,12 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
|
||||
add_executable(test_generator tests/test_generator.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||
target_include_directories(test_generator PRIVATE src)
|
||||
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_generator COMMAND test_generator)
|
||||
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.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)
|
||||
target_include_directories(test_context_menu PRIVATE src)
|
||||
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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -215,8 +292,32 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
|
||||
add_executable(test_source_management tests/test_source_management.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/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
|
||||
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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
@@ -224,11 +325,11 @@ if(BUILD_TESTING)
|
||||
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/controller.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)
|
||||
target_include_directories(test_new_features PRIVATE src)
|
||||
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)
|
||||
@@ -238,11 +339,11 @@ if(BUILD_TESTING)
|
||||
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/controller.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)
|
||||
target_include_directories(test_type_selector PRIVATE src)
|
||||
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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -251,29 +352,62 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
|
||||
add_executable(test_theme tests/test_theme.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_theme PRIVATE src)
|
||||
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_theme COMMAND test_theme)
|
||||
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
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/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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_windbg_provider PRIVATE dbgeng ole32)
|
||||
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
add_test(NAME test_type_visibility COMMAND test_type_visibility)
|
||||
|
||||
# Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect
|
||||
# Requires a running WinDbg debug server on port 5055
|
||||
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)
|
||||
|
||||
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/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
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
add_executable(test_com_security tests/test_com_security.cpp)
|
||||
target_link_libraries(test_com_security PRIVATE dbgeng ole32 version)
|
||||
add_test(NAME test_com_security COMMAND test_com_security)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
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_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
|
||||
${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)
|
||||
|
||||
# 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)
|
||||
@@ -287,6 +421,12 @@ if(BUILD_TESTING)
|
||||
COMMENT "Deploying Qt runtime DLLs for tests..."
|
||||
)
|
||||
endif()
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemoryWindows)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
|
||||
145
README.md
145
README.md
@@ -1,49 +1,128 @@
|
||||
This tool 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 runtime or from some static source.
|
||||
<div align="center">
|
||||
|
||||

|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/RECLASS_LIGHTMODE.svg" height="170">
|
||||
<img src="docs/RECLASS_DARKMODE.svg" alt="Reclass" height="170" />
|
||||
</picture>
|
||||
|
||||
## State
|
||||
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>Built from scratch as a modern replacement for ReClass.NET and ReClassEx**
|
||||
|
||||
- MCP (Model Context Protocol) bridge via `ReclassMcpBridge.exe`. The server starts by default and can be stopped from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code) and falls back to UI prompts when the client requests something not yet covered by tools. To connect, add this to your MCP client config (e.g. `.mcp.json`):
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ReclassMcpBridge": {
|
||||
"command": "path/to/build/ReclassMcpBridge.exe",
|
||||
"args": []
|
||||
}
|
||||
[Download](https://github.com/IChooseYou/Reclass/releases) · [Build Instructions](#build) · [MCP Integration](#mcp-integration) · [Alternatives](#alternatives)
|
||||
|
||||
[](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.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
- **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
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] 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
|
||||
|
||||
## 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
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ReclassMcpBridge": {
|
||||
"command": "path/to/build/ReclassMcpBridge",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
```
|
||||
- Plugin system is partially implemented. Some UI bugs exist.
|
||||
- Vector/Matrix improvements have been made but are not entirely complete.
|
||||
- Every edit goes through a full undo/redo system.
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
1. Prerequisites
|
||||
### Prerequisites
|
||||
|
||||
- Qt 6 with MinGW - Qt Online Installer https://doc.qt.io/qt-6/qt-online-installation.html , note to select MinGW kit + CMake/Ninja from Tools section (online installers index: https://download.qt.io/official_releases/online_installers/)
|
||||
- CMake 3.20+ - https://cmake.org/download/ - bundled with Qt
|
||||
- windeployqt docs - https://doc.qt.io/qt-6/windows-deployment.html
|
||||
- **Qt 6** (or Qt 5) with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
|
||||
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
|
||||
- **Ninja** — bundled with the Qt installer
|
||||
|
||||
2. Quick Build (relies on powershell| for manual build skip to step 3)
|
||||
### Quick Build
|
||||
|
||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||
cd Reclass
|
||||
.\scripts\build_qscintilla.ps1
|
||||
.\scripts\build.ps1
|
||||
^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||
cd Reclass
|
||||
.\scripts\build_qscintilla.ps1
|
||||
.\scripts\build.ps1
|
||||
```
|
||||
|
||||
3. Manual Build
|
||||
The build script auto-detects your Qt install location.
|
||||
|
||||
Step by step for peoplewho want to run commands themselves:
|
||||
1. Clone with --recurse-submodules (+ fallback git submodule update --init --recursive)
|
||||
2. Build QScintilla: qmake + mingw32-make in third_party/qscintilla/src
|
||||
3. CMake configure + build with -DCMAKE_PREFIX_PATH
|
||||
4. optionallly windeployqt the exe
|
||||
### Manual Build
|
||||
|
||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||
2. Build QScintilla: `qmake` + `mingw32-make` in `third_party/qscintilla/src`
|
||||
3. Configure and build:
|
||||
```bash
|
||||
cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/mingw_64
|
||||
cmake --build build
|
||||
```
|
||||
4. Optionally run `windeployqt` on the output executable
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
- ReClass.NET (reclass.net) - https://github.com/ReClassNET/ReClass.NET
|
||||
- ReClassEx - https://github.com/ajkhoury/ReClassEx
|
||||
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
|
||||
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License</sub>
|
||||
</div>
|
||||
|
||||
8
deploy/Reclass.desktop
Normal file
8
deploy/Reclass.desktop
Normal file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Reclass
|
||||
Comment=Memory structure reverse engineering tool
|
||||
Exec=Reclass
|
||||
Icon=reclass
|
||||
Categories=Development;Debugger;
|
||||
Terminal=false
|
||||
BIN
docs/README_PIC1.png
Normal file
BIN
docs/README_PIC1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/README_PIC2.png
Normal file
BIN
docs/README_PIC2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/README_PIC3.png
Normal file
BIN
docs/README_PIC3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
160
docs/RECLASS_DARKMODE.svg
Normal file
160
docs/RECLASS_DARKMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 186.01 52.79">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#Unbenannter_Verlauf_130-2);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_236-2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#Unbenannter_Verlauf_225-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #1f2939;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #5d9bd4;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #1e3e88;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #6e809a;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#Unbenannter_Verlauf_225);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: url(#Unbenannter_Verlauf_236);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: url(#Unbenannter_Verlauf_130);
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: url(#Unbenannter_Verlauf_170);
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: url(#Unbenannter_Verlauf_161);
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: url(#Unbenannter_Verlauf_183);
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #b06ba9;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #826415;
|
||||
}
|
||||
|
||||
.cls-16 {
|
||||
fill: #e2aa11;
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #893089;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.33" y1="8.33" x2="18.11" y2="18.11" gradientTransform="translate(13.22 -5.47) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f3db78"/>
|
||||
<stop offset=".19" stop-color="#f4e188"/>
|
||||
<stop offset=".34" stop-color="#f4e38d"/>
|
||||
<stop offset=".38" stop-color="#f4df81"/>
|
||||
<stop offset=".47" stop-color="#f5d86f"/>
|
||||
<stop offset=".57" stop-color="#f5d463"/>
|
||||
<stop offset=".67" stop-color="#f6d360"/>
|
||||
<stop offset=".89" stop-color="#f1cc53"/>
|
||||
<stop offset="1" stop-color="#efbe33"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.46" x2="10.98" y2="26.03" gradientTransform="translate(-4.95 39.45) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".18" stop-color="#e2aa11"/>
|
||||
<stop offset=".91" stop-color="#826415"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.46" y1=".41" x2="26.03" y2="10.98" gradientTransform="translate(31.39 24.39) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.97" y1="15.65" x2="42.34" y2="23.02" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#deb0d3"/>
|
||||
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||
<stop offset=".67" stop-color="#cd99c5"/>
|
||||
<stop offset=".89" stop-color="#c68abc"/>
|
||||
<stop offset="1" stop-color="#bb7db4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.78" y1="20.14" x2="36.87" y2="28.24" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#b06ba9"/>
|
||||
<stop offset=".87" stop-color="#893089"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.45" y1="9.43" x2="47.55" y2="17.53" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.88" y1="39.45" x2="42.29" y2="46.86" gradientTransform="translate(41.82 -14.64) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#91c4eb"/>
|
||||
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||
<stop offset=".33" stop-color="#96c6ec"/>
|
||||
<stop offset=".35" stop-color="#91c3ea"/>
|
||||
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||
<stop offset=".56" stop-color="#73b2e2"/>
|
||||
<stop offset=".67" stop-color="#70b0e1"/>
|
||||
<stop offset=".89" stop-color="#60a7dc"/>
|
||||
<stop offset="1" stop-color="#4d9bd5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.83" y1="43.9" x2="36.92" y2="51.99" gradientTransform="translate(22.68 105.31) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||
<stop offset=".87" stop-color="#1e3e88"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.51" y1="33.23" x2="47.59" y2="41.32" gradientTransform="translate(48.45 94.63) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||
</defs>
|
||||
<g>
|
||||
<rect class="cls-7" x="22.48" y="16.85" width="1.76" height="25.1"/>
|
||||
<rect class="cls-7" x="17.08" y="16.85" width="19.7" height="1.82"/>
|
||||
<rect class="cls-7" x="22.48" y="40.19" width="11.9" height="1.76"/>
|
||||
<g>
|
||||
<rect class="cls-12" x="2.56" y="6.31" width="21.31" height="13.82" transform="translate(-5.48 13.22) rotate(-45)"/>
|
||||
<rect class="cls-15" x="17.52" y="6.88" width="1.15" height="22.44" transform="translate(18.1 -7.49) rotate(45)"/>
|
||||
<g>
|
||||
<rect class="cls-16" x="7.76" y="-2.88" width="1.15" height="22.44" transform="translate(8.34 -3.45) rotate(45)"/>
|
||||
<rect class="cls-10" x="5.12" y="13.27" width="1.15" height="14.95" transform="translate(24.39 31.39) rotate(135)"/>
|
||||
<rect class="cls-1" x="20.17" y="-1.78" width="1.15" height="14.95" transform="translate(39.45 -4.95) rotate(135)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-11" points="40.33 10.29 29.64 21.02 36.98 28.38 47.67 17.66 40.33 10.29"/>
|
||||
<polygon class="cls-17" points="37.01 29.1 36.29 28.38 47.68 16.96 48.39 17.68 37.01 29.1"/>
|
||||
<polygon class="cls-14" points="29.67 21.74 28.95 21.02 40.34 9.6 41.05 10.31 29.67 21.74"/>
|
||||
<polygon class="cls-8" points="28.95 21.02 29.67 20.3 37.72 28.38 37 29.1 28.95 21.02"/>
|
||||
<polygon class="cls-3" points="39.63 10.31 40.34 9.6 48.39 17.67 47.68 18.39 39.63 10.31"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-13" x="30.96" y="37.92" width="15.26" height="10.48" transform="translate(-19.21 39.92) rotate(-45)"/>
|
||||
<g>
|
||||
<rect class="cls-9" x="32.83" y="42.71" width="1.01" height="11.38" transform="translate(91.13 59.06) rotate(135)"/>
|
||||
<rect class="cls-2" x="43.5" y="32.04" width="1.01" height="11.38" transform="translate(101.81 33.29) rotate(135)"/>
|
||||
<rect class="cls-6" x="41.84" y="38.69" width="1.01" height="16.1" transform="translate(45.45 -16.25) rotate(45)"/>
|
||||
<rect class="cls-5" x="34.5" y="31.35" width="1.01" height="16.1" transform="translate(38.11 -13.21) rotate(45)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-4" d="M53.66,30.51v11.46h-2.57v-25.13h9.36c5.04,0,7.72,2.72,7.72,6.65,0,3.22-1.89,5.26-4.51,5.89,2.34.58,4.09,2.2,4.09,6.5v1.02c0,1.74-.11,4.07.33,5.07h-2.54c-.46-1.08-.39-3.1-.39-5.34v-.6c0-3.87-1.12-5.52-5.79-5.52h-5.7ZM53.66,28.26h5.79c4.15,0,6.03-1.56,6.03-4.64,0-2.9-1.89-4.54-5.57-4.54h-6.25v9.18Z"/>
|
||||
<path class="cls-4" d="M86.55,29.87h-12.65v9.79h13.88l-.35,2.3h-16.06v-25.12h15.81v2.27h-13.28v8.49h12.65v2.27Z"/>
|
||||
<path class="cls-4" d="M109.12,35.04c-1.15,4.11-4.2,7.19-9.68,7.19-7.34,0-11.13-5.72-11.13-12.79s3.76-12.96,11.21-12.96c5.64,0,8.84,3.18,9.63,7.37h-2.56c-1.04-3.02-3.01-5.13-7.18-5.13-5.92,0-8.38,5.4-8.38,10.66s2.39,10.62,8.52,10.62c3.99,0,5.89-2.16,7.01-4.95h2.57Z"/>
|
||||
<path class="cls-4" d="M111.62,16.84h2.56v22.82h13.3l-.41,2.27h-15.46v-25.09Z"/>
|
||||
<path class="cls-4" d="M133.03,33.77l-2.97,8.16h-2.58l9.09-25.09h3.11l9.48,25.09h-2.76l-3.05-8.16h-10.32ZM142.61,31.5c-2.61-7.07-3.99-10.62-4.51-12.4h-.04c-.61,2-2.16,6.36-4.27,12.4h8.82Z"/>
|
||||
<path class="cls-4" d="M151.68,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.24,4.56,8.24,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||
<path class="cls-4" d="M170.59,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.25,4.56,8.25,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
160
docs/RECLASS_LIGHTMODE.svg
Normal file
160
docs/RECLASS_LIGHTMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.55 52.66">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#Unbenannter_Verlauf_130-2);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_236-2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#Unbenannter_Verlauf_225-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #5d9bd4;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #e3e8f0;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #1e3e88;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #6e809a;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#Unbenannter_Verlauf_225);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: url(#Unbenannter_Verlauf_236);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: url(#Unbenannter_Verlauf_130);
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: url(#Unbenannter_Verlauf_170);
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: url(#Unbenannter_Verlauf_161);
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: url(#Unbenannter_Verlauf_183);
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #b06ba9;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #826415;
|
||||
}
|
||||
|
||||
.cls-16 {
|
||||
fill: #e2aa11;
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #893089;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.31" y1="8.31" x2="18.06" y2="18.06" gradientTransform="translate(13.19 -5.46) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f3db78"/>
|
||||
<stop offset=".19" stop-color="#f4e188"/>
|
||||
<stop offset=".34" stop-color="#f4e38d"/>
|
||||
<stop offset=".38" stop-color="#f4df81"/>
|
||||
<stop offset=".47" stop-color="#f5d86f"/>
|
||||
<stop offset=".57" stop-color="#f5d463"/>
|
||||
<stop offset=".67" stop-color="#f6d360"/>
|
||||
<stop offset=".89" stop-color="#f1cc53"/>
|
||||
<stop offset="1" stop-color="#efbe33"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.42" x2="10.95" y2="25.97" gradientTransform="translate(-4.94 39.35) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".18" stop-color="#e2aa11"/>
|
||||
<stop offset=".91" stop-color="#826415"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.42" y1=".41" x2="25.97" y2="10.95" gradientTransform="translate(31.32 24.33) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.88" y1="15.61" x2="42.24" y2="22.97" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#deb0d3"/>
|
||||
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||
<stop offset=".67" stop-color="#cd99c5"/>
|
||||
<stop offset=".89" stop-color="#c68abc"/>
|
||||
<stop offset="1" stop-color="#bb7db4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.7" y1="20.09" x2="36.78" y2="28.17" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#b06ba9"/>
|
||||
<stop offset=".87" stop-color="#893089"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.35" y1="9.41" x2="47.43" y2="17.49" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.79" y1="39.35" x2="42.18" y2="46.74" gradientTransform="translate(41.71 -14.61) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#91c4eb"/>
|
||||
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||
<stop offset=".33" stop-color="#96c6ec"/>
|
||||
<stop offset=".35" stop-color="#91c3ea"/>
|
||||
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||
<stop offset=".56" stop-color="#73b2e2"/>
|
||||
<stop offset=".67" stop-color="#70b0e1"/>
|
||||
<stop offset=".89" stop-color="#60a7dc"/>
|
||||
<stop offset="1" stop-color="#4d9bd5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.76" y1="43.79" x2="36.83" y2="51.86" gradientTransform="translate(22.62 105.05) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||
<stop offset=".87" stop-color="#1e3e88"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.41" y1="33.15" x2="47.47" y2="41.21" gradientTransform="translate(48.33 94.4) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||
</defs>
|
||||
<g>
|
||||
<rect class="cls-7" x="22.43" y="16.81" width="1.75" height="25.04"/>
|
||||
<rect class="cls-7" x="17.04" y="16.81" width="19.66" height="1.82"/>
|
||||
<rect class="cls-7" x="22.43" y="40.09" width="11.87" height="1.75"/>
|
||||
<g>
|
||||
<rect class="cls-12" x="2.56" y="6.29" width="21.26" height="13.79" transform="translate(-5.46 13.19) rotate(-45)"/>
|
||||
<rect class="cls-15" x="17.48" y="6.87" width="1.15" height="22.38" transform="translate(18.06 -7.47) rotate(45)"/>
|
||||
<g>
|
||||
<rect class="cls-16" x="7.74" y="-2.87" width="1.15" height="22.38" transform="translate(8.32 -3.45) rotate(45)"/>
|
||||
<rect class="cls-10" x="5.1" y="13.24" width="1.15" height="14.92" transform="translate(24.33 31.32) rotate(135)"/>
|
||||
<rect class="cls-1" x="20.12" y="-1.78" width="1.15" height="14.92" transform="translate(39.35 -4.94) rotate(135)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-11" points="40.23 10.26 29.56 20.96 36.89 28.31 47.56 17.61 40.23 10.26"/>
|
||||
<polygon class="cls-17" points="36.91 29.03 36.2 28.31 47.56 16.92 48.27 17.63 36.91 29.03"/>
|
||||
<polygon class="cls-14" points="29.59 21.68 28.88 20.97 40.24 9.57 40.95 10.29 29.59 21.68"/>
|
||||
<polygon class="cls-8" points="28.88 20.97 29.59 20.25 37.62 28.31 36.91 29.02 28.88 20.97"/>
|
||||
<polygon class="cls-3" points="39.53 10.29 40.24 9.57 48.27 17.63 47.56 18.34 39.53 10.29"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-13" x="30.88" y="37.82" width="15.22" height="10.45" transform="translate(-19.17 39.82) rotate(-45)"/>
|
||||
<g>
|
||||
<rect class="cls-9" x="32.75" y="42.61" width="1.01" height="11.36" transform="translate(90.91 58.91) rotate(135)"/>
|
||||
<rect class="cls-2" x="43.4" y="31.96" width="1.01" height="11.36" transform="translate(101.56 33.21) rotate(135)"/>
|
||||
<rect class="cls-6" x="41.73" y="38.59" width="1.01" height="16.06" transform="translate(45.34 -16.21) rotate(45)"/>
|
||||
<rect class="cls-4" x="34.41" y="31.27" width="1.01" height="16.06" transform="translate(38.02 -13.18) rotate(45)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-5" d="M53.53,30.44v11.43h-2.56v-25.07h9.34c5.03,0,7.7,2.71,7.7,6.63,0,3.21-1.88,5.24-4.5,5.87,2.33.58,4.08,2.19,4.08,6.49v1.01c0,1.74-.11,4.06.33,5.06h-2.54c-.46-1.08-.39-3.09-.39-5.33v-.59c0-3.87-1.12-5.51-5.78-5.51h-5.68ZM53.53,28.19h5.77c4.14,0,6.02-1.55,6.02-4.63,0-2.89-1.88-4.53-5.55-4.53h-6.24v9.16Z"/>
|
||||
<path class="cls-5" d="M86.34,29.8h-12.62v9.77h13.84l-.35,2.3h-16.02v-25.06h15.77v2.26h-13.25v8.47h12.62v2.26Z"/>
|
||||
<path class="cls-5" d="M108.85,34.96c-1.15,4.09-4.19,7.17-9.65,7.17-7.32,0-11.11-5.7-11.11-12.76s3.75-12.93,11.18-12.93c5.63,0,8.82,3.17,9.6,7.35h-2.56c-1.03-3.02-3-5.12-7.16-5.12-5.91,0-8.36,5.39-8.36,10.63s2.38,10.6,8.5,10.6c3.98,0,5.88-2.16,6.99-4.94h2.56Z"/>
|
||||
<path class="cls-5" d="M111.34,16.8h2.56v22.77h13.27l-.4,2.26h-15.42v-25.03Z"/>
|
||||
<path class="cls-5" d="M132.69,33.69l-2.96,8.14h-2.57l9.07-25.03h3.1l9.45,25.03h-2.75l-3.04-8.14h-10.3ZM142.25,31.43c-2.61-7.05-3.98-10.59-4.5-12.37h-.04c-.61,2-2.15,6.34-4.26,12.37h8.8Z"/>
|
||||
<path class="cls-5" d="M151.31,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||
<path class="cls-5" d="M170.17,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(ProcessMemoryWindowsPlugin LANGUAGES CXX)
|
||||
project(ProcessMemoryPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Plugin sources
|
||||
set(PLUGIN_SOURCES
|
||||
ProcessMemoryWindowsPlugin.h
|
||||
ProcessMemoryWindowsPlugin.cpp
|
||||
ProcessMemoryPlugin.h
|
||||
ProcessMemoryPlugin.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
|
||||
)
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES})
|
||||
add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
# Link Qt
|
||||
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
|
||||
# Platform-specific linking
|
||||
if(WIN32)
|
||||
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32)
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
|
||||
endif()
|
||||
|
||||
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden)
|
||||
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
# Include directories
|
||||
target_include_directories(ProcessMemoryWindowsPlugin PRIVATE
|
||||
target_include_directories(ProcessMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
# Output to Plugins folder
|
||||
set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES
|
||||
set_target_properties(ProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "ProcessMemoryWindowsPlugin.h"
|
||||
#include "ProcessMemoryPlugin.h"
|
||||
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryWindowsProvider implementation
|
||||
// ProcessMemoryProvider implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_handle(nullptr)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
@@ -60,28 +60,28 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_handle || len <= 0) return false;
|
||||
|
||||
SIZE_T bytesRead = 0;
|
||||
ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead);
|
||||
ReadProcessMemory(m_handle, (LPCVOID)(addr), buf, (SIZE_T)len, &bytesRead);
|
||||
if ((int)bytesRead < len)
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
return bytesRead > 0;
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_handle || !m_writable || len <= 0) return false;
|
||||
|
||||
SIZE_T bytesWritten = 0;
|
||||
if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten))
|
||||
if (WriteProcessMemory(m_handle, (LPVOID)(addr), buf, (SIZE_T)len, &bytesWritten))
|
||||
return bytesWritten == (SIZE_T)len;
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
@@ -96,7 +96,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
return {};
|
||||
}
|
||||
|
||||
void ProcessMemoryWindowsProvider::cacheModules()
|
||||
void ProcessMemoryProvider::cacheModules()
|
||||
{
|
||||
HMODULE mods[1024];
|
||||
DWORD needed = 0;
|
||||
@@ -126,7 +126,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_fd(-1)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
@@ -152,19 +152,17 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
|
||||
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (m_fd < 0 || len <= 0) return false;
|
||||
|
||||
uint64_t absAddr = m_base + addr;
|
||||
|
||||
// Try process_vm_readv first (faster, no fd seek contention)
|
||||
struct iovec local;
|
||||
local.iov_base = buf;
|
||||
local.iov_len = static_cast<size_t>(len);
|
||||
|
||||
struct iovec remote;
|
||||
remote.iov_base = reinterpret_cast<void*>(absAddr);
|
||||
remote.iov_base = reinterpret_cast<void*>(addr);
|
||||
remote.iov_len = static_cast<size_t>(len);
|
||||
|
||||
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
|
||||
@@ -172,23 +170,21 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
return true;
|
||||
|
||||
// Fallback: pread on /proc/<pid>/mem
|
||||
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
|
||||
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
|
||||
return nread == static_cast<ssize_t>(len);
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (m_fd < 0 || !m_writable || len <= 0) return false;
|
||||
|
||||
uint64_t absAddr = m_base + addr;
|
||||
|
||||
// Try process_vm_writev first
|
||||
struct iovec local;
|
||||
local.iov_base = const_cast<void*>(buf);
|
||||
local.iov_len = static_cast<size_t>(len);
|
||||
|
||||
struct iovec remote;
|
||||
remote.iov_base = reinterpret_cast<void*>(absAddr);
|
||||
remote.iov_base = reinterpret_cast<void*>(addr);
|
||||
remote.iov_len = static_cast<size_t>(len);
|
||||
|
||||
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
|
||||
@@ -196,11 +192,11 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len
|
||||
return true;
|
||||
|
||||
// Fallback: pwrite on /proc/<pid>/mem
|
||||
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
|
||||
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
|
||||
return nwritten == static_cast<ssize_t>(len);
|
||||
}
|
||||
|
||||
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
@@ -215,7 +211,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
return {};
|
||||
}
|
||||
|
||||
void ProcessMemoryWindowsProvider::cacheModules()
|
||||
void ProcessMemoryProvider::cacheModules()
|
||||
{
|
||||
// Parse /proc/<pid>/maps to discover loaded modules
|
||||
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
|
||||
@@ -288,7 +284,16 @@ void ProcessMemoryWindowsProvider::cacheModules()
|
||||
|
||||
#endif // platform
|
||||
|
||||
ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
|
||||
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
|
||||
return mod.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
ProcessMemoryProvider::~ProcessMemoryProvider()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_handle)
|
||||
@@ -299,7 +304,7 @@ ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
|
||||
#endif
|
||||
}
|
||||
|
||||
int ProcessMemoryWindowsProvider::size() const
|
||||
int ProcessMemoryProvider::size() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return m_handle ? 0x10000 : 0;
|
||||
@@ -309,22 +314,22 @@ int ProcessMemoryWindowsProvider::size() const
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryWindowsPlugin implementation
|
||||
// ProcessMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
QIcon ProcessMemoryWindowsPlugin::Icon() const
|
||||
QIcon ProcessMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsPlugin::canHandle(const QString& target) const
|
||||
bool ProcessMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
// Target format: "pid:name" or just "pid"
|
||||
QRegularExpression re("^\\d+");
|
||||
return re.match(target).hasMatch();
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
// Parse target: "pid:name" or just "pid"
|
||||
QStringList parts = target.split(':');
|
||||
@@ -339,7 +344,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
|
||||
|
||||
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
|
||||
|
||||
auto provider = std::make_unique<ProcessMemoryWindowsProvider>(pid, name);
|
||||
auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
|
||||
if (!provider->isValid())
|
||||
{
|
||||
if (errorMsg)
|
||||
@@ -352,7 +357,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
|
||||
return provider;
|
||||
}
|
||||
|
||||
uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const
|
||||
uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Parse PID from target
|
||||
@@ -409,7 +414,7 @@ uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
// Use custom process enumeration from plugin
|
||||
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||
@@ -440,7 +445,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
|
||||
QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> processes;
|
||||
|
||||
@@ -543,5 +548,5 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new ProcessMemoryWindowsPlugin();
|
||||
return new ProcessMemoryPlugin();
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Process memory provider (Windows)
|
||||
* Reads/writes memory from a live process using Windows platform APIs
|
||||
* Process memory provider
|
||||
* Reads/writes memory from a live process using platform APIs
|
||||
*/
|
||||
class ProcessMemoryWindowsProvider : public rcx::Provider
|
||||
class ProcessMemoryProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName);
|
||||
~ProcessMemoryWindowsProvider() override;
|
||||
ProcessMemoryProvider(uint32_t pid, const QString& processName);
|
||||
~ProcessMemoryProvider() override;
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
@@ -24,14 +24,20 @@ public:
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("LocalProcess"); }
|
||||
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; }
|
||||
void setBase(uint64_t b) override { m_base = b; }
|
||||
bool isReadable(uint64_t, int len) const override {
|
||||
#ifdef _WIN32
|
||||
return m_handle && len >= 0;
|
||||
#elif defined(__linux__)
|
||||
return m_fd >= 0 && len >= 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Process-specific helpers
|
||||
uint32_t pid() const { return m_pid; }
|
||||
uint64_t baseAddress() const { return m_base; }
|
||||
void refreshModules() { m_modules.clear(); cacheModules(); }
|
||||
|
||||
private:
|
||||
@@ -57,15 +63,15 @@ private:
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin that provides ProcessMemoryWindowsProvider
|
||||
* Plugin that provides ProcessMemoryProvider
|
||||
*/
|
||||
class ProcessMemoryWindowsPlugin : public IProviderPlugin
|
||||
class ProcessMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
std::string Name() const override { return "Process Memory Windows"; }
|
||||
std::string Name() const override { return "Process 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 from local running processes (Windows)"; }
|
||||
std::string Description() const override { return "Read and write memory from local running processes"; }
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal file
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(RcNetCompatPlugin 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 ON)
|
||||
|
||||
# Plugin sources
|
||||
set(PLUGIN_SOURCES
|
||||
RcNetCompatPlugin.h
|
||||
RcNetCompatPlugin.cpp
|
||||
RcNetCompatProvider.h
|
||||
RcNetCompatProvider.cpp
|
||||
ReClassNET_Plugin.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
|
||||
)
|
||||
|
||||
# -- Optional .NET bridge -------------------------------------------------
|
||||
# When the .NET SDK is available, build the C# bridge assembly and enable
|
||||
# CLR hosting support in the C++ plugin.
|
||||
|
||||
find_program(DOTNET_EXE dotnet)
|
||||
if(DOTNET_EXE)
|
||||
# Check that 'dotnet build' actually works for net472
|
||||
execute_process(
|
||||
COMMAND ${DOTNET_EXE} --list-sdks
|
||||
OUTPUT_VARIABLE _dotnet_sdks
|
||||
ERROR_QUIET
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
if(_dotnet_sdks)
|
||||
set(HAS_CLR_BRIDGE ON)
|
||||
message(STATUS "RcNetCompat: .NET SDK found -- building managed bridge")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(HAS_CLR_BRIDGE)
|
||||
list(APPEND PLUGIN_SOURCES
|
||||
ClrHost.h
|
||||
ClrHost.cpp
|
||||
)
|
||||
|
||||
# Build the C# bridge assembly
|
||||
set(_bridge_src "${CMAKE_CURRENT_SOURCE_DIR}/bridge")
|
||||
set(_bridge_out "${CMAKE_BINARY_DIR}/Plugins/RcNetBridge.dll")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_bridge_out}"
|
||||
COMMAND ${DOTNET_EXE} build
|
||||
"${_bridge_src}/RcNetBridge.csproj"
|
||||
-c Release
|
||||
-o "${CMAKE_BINARY_DIR}/Plugins"
|
||||
--nologo -v quiet
|
||||
DEPENDS
|
||||
"${_bridge_src}/RcNetBridge.cs"
|
||||
"${_bridge_src}/RcNetBridge.csproj"
|
||||
COMMENT "Building RcNetBridge.dll (.NET bridge)..."
|
||||
)
|
||||
add_custom_target(RcNetBridge ALL DEPENDS "${_bridge_out}")
|
||||
else()
|
||||
message(STATUS "RcNetCompat: .NET SDK not found -- managed plugin support disabled")
|
||||
endif()
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(RcNetCompatPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
if(HAS_CLR_BRIDGE)
|
||||
target_compile_definitions(RcNetCompatPlugin PRIVATE HAS_CLR_BRIDGE=1)
|
||||
add_dependencies(RcNetCompatPlugin RcNetBridge)
|
||||
# CLR hosting uses COM (ole32)
|
||||
target_link_libraries(RcNetCompatPlugin PRIVATE ole32)
|
||||
endif()
|
||||
|
||||
# Link Qt
|
||||
target_link_libraries(RcNetCompatPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
|
||||
# Include directories
|
||||
target_include_directories(RcNetCompatPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
# Output to Plugins folder
|
||||
set_target_properties(RcNetCompatPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal file
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "ClrHost.h"
|
||||
|
||||
#include <cwchar>
|
||||
|
||||
// -- GUIDs ----------------------------------------------------------------
|
||||
|
||||
using FnCLRCreateInstance = HRESULT(STDAPICALLTYPE*)(REFCLSID, REFIID, LPVOID*);
|
||||
|
||||
// {9280188D-0E8E-4867-B30C-7FA83884E8DE}
|
||||
static const GUID sCLSID_CLRMetaHost =
|
||||
{0x9280188d, 0x0e8e, 0x4867, {0xb3, 0x0c, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde}};
|
||||
|
||||
// {D332DB9E-B9B3-4125-8207-A14884F53216}
|
||||
static const GUID sIID_ICLRMetaHost =
|
||||
{0xD332DB9E, 0xB9B3, 0x4125, {0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16}};
|
||||
|
||||
// {BD39D1D2-BA2F-486A-89B0-B4B0CB466891}
|
||||
static const GUID sIID_ICLRRuntimeInfo =
|
||||
{0xBD39D1D2, 0xBA2F, 0x486a, {0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91}};
|
||||
|
||||
// {90F1A06E-7712-4762-86B5-7A5EBA6BDB02}
|
||||
static const GUID sCLSID_CLRRuntimeHost =
|
||||
{0x90F1A06E, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
|
||||
|
||||
// {90F1A06C-7712-4762-86B5-7A5EBA6BDB02}
|
||||
static const GUID sIID_ICLRRuntimeHost =
|
||||
{0x90F1A06C, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
|
||||
|
||||
// -- ClrHost implementation -----------------------------------------------
|
||||
|
||||
ClrHost::ClrHost()
|
||||
{
|
||||
startClr();
|
||||
}
|
||||
|
||||
ClrHost::~ClrHost()
|
||||
{
|
||||
if (m_runtimeHost) m_runtimeHost->Release();
|
||||
if (m_runtimeInfo) m_runtimeInfo->Release();
|
||||
if (m_metaHost) m_metaHost->Release();
|
||||
if (m_mscoree) FreeLibrary(m_mscoree);
|
||||
}
|
||||
|
||||
bool ClrHost::startClr()
|
||||
{
|
||||
m_mscoree = LoadLibraryW(L"mscoree.dll");
|
||||
if (!m_mscoree)
|
||||
return false;
|
||||
|
||||
auto fnCreate = reinterpret_cast<FnCLRCreateInstance>(
|
||||
GetProcAddress(m_mscoree, "CLRCreateInstance"));
|
||||
if (!fnCreate)
|
||||
return false;
|
||||
|
||||
HRESULT hr = fnCreate(sCLSID_CLRMetaHost, sIID_ICLRMetaHost,
|
||||
reinterpret_cast<LPVOID*>(&m_metaHost));
|
||||
if (FAILED(hr) || !m_metaHost)
|
||||
return false;
|
||||
|
||||
hr = m_metaHost->GetRuntime(L"v4.0.30319", sIID_ICLRRuntimeInfo,
|
||||
reinterpret_cast<LPVOID*>(&m_runtimeInfo));
|
||||
if (FAILED(hr) || !m_runtimeInfo)
|
||||
return false;
|
||||
|
||||
hr = m_runtimeInfo->GetInterface(sCLSID_CLRRuntimeHost, sIID_ICLRRuntimeHost,
|
||||
(LPVOID*)&m_runtimeHost);
|
||||
if (FAILED(hr) || !m_runtimeHost)
|
||||
return false;
|
||||
|
||||
hr = m_runtimeHost->Start();
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
m_clrStarted = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClrHost::loadManagedPlugin(const QString& bridgeDllPath,
|
||||
const QString& pluginPath,
|
||||
RcNetFunctions* outFunctions,
|
||||
QString* errorMsg)
|
||||
{
|
||||
if (!m_runtimeHost || !m_clrStarted) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
".NET Framework 4.x is not available on this machine.\n"
|
||||
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Zero the function table -- the bridge will fill it
|
||||
memset(outFunctions, 0, sizeof(RcNetFunctions));
|
||||
|
||||
// Build the argument string: "<hex_address_of_function_table>|<plugin_path>"
|
||||
// Use %ls (not %s) for wide strings -- MinGW follows POSIX conventions.
|
||||
wchar_t arg[2048];
|
||||
swprintf(arg, sizeof(arg) / sizeof(wchar_t),
|
||||
L"%llx|%ls",
|
||||
reinterpret_cast<unsigned long long>(outFunctions),
|
||||
reinterpret_cast<const wchar_t*>(pluginPath.utf16()));
|
||||
|
||||
DWORD retVal = 0;
|
||||
HRESULT hr = m_runtimeHost->ExecuteInDefaultAppDomain(
|
||||
reinterpret_cast<LPCWSTR>(bridgeDllPath.utf16()),
|
||||
L"RcNetBridge.Bridge",
|
||||
L"Initialize",
|
||||
arg,
|
||||
&retVal
|
||||
);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"Failed to execute .NET bridge (HRESULT 0x%1).\n"
|
||||
"Bridge: %2\n"
|
||||
"Plugin: %3")
|
||||
.arg(static_cast<uint>(hr), 8, 16, QChar('0'))
|
||||
.arg(bridgeDllPath)
|
||||
.arg(pluginPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (retVal != 0) {
|
||||
if (errorMsg) {
|
||||
switch (retVal) {
|
||||
case 1:
|
||||
*errorMsg = QStringLiteral("Bridge: invalid argument format.");
|
||||
break;
|
||||
case 2:
|
||||
*errorMsg = QStringLiteral(
|
||||
"No ICoreProcessFunctions implementation found in the .NET plugin.\n"
|
||||
"The DLL may not be a ReClass.NET plugin.");
|
||||
break;
|
||||
case 3:
|
||||
*errorMsg = QStringLiteral(
|
||||
"Failed to load the .NET plugin assembly.\n"
|
||||
"Check that all its dependencies are available.");
|
||||
break;
|
||||
default:
|
||||
*errorMsg = QStringLiteral("Bridge returned error code %1.").arg(retVal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the bridge wrote at least the minimum required function pointers
|
||||
if (!outFunctions->ReadRemoteMemory ||
|
||||
!outFunctions->OpenRemoteProcess ||
|
||||
!outFunctions->EnumerateProcesses ||
|
||||
!outFunctions->CloseRemoteProcess) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"The .NET bridge loaded but did not provide the required functions "
|
||||
"(ReadRemoteMemory, OpenRemoteProcess, CloseRemoteProcess, EnumerateProcesses).");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal file
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
// In-process CLR hosting for loading .NET ReClass.NET plugins.
|
||||
// Dynamically loads mscoree.dll and uses ICLRMetaHost -> ICLRRuntimeInfo ->
|
||||
// ICLRRuntimeHost::ExecuteInDefaultAppDomain to call into the C# bridge.
|
||||
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
#include <QString>
|
||||
#include <windows.h>
|
||||
#include <objbase.h>
|
||||
|
||||
// -- Minimal COM interface definitions for CLR hosting --------------------
|
||||
// Defined here to avoid depending on Windows SDK metahost.h / mscoree.h
|
||||
// which may not be present in all MinGW distributions.
|
||||
// Only methods we actually call have real signatures; the rest are stubs
|
||||
// that preserve correct vtable offsets.
|
||||
|
||||
#undef INTERFACE
|
||||
#define INTERFACE ICLRMetaHost
|
||||
DECLARE_INTERFACE_(ICLRMetaHost, IUnknown)
|
||||
{
|
||||
// IUnknown
|
||||
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||
STDMETHOD_(ULONG, Release)() PURE;
|
||||
// ICLRMetaHost
|
||||
STDMETHOD(GetRuntime)(LPCWSTR pwzVersion, REFIID riid, LPVOID* ppRuntime) PURE;
|
||||
STDMETHOD(GetVersionFromFile)(LPCWSTR, LPWSTR, DWORD*) PURE;
|
||||
STDMETHOD(EnumerateInstalledRuntimes)(void**) PURE;
|
||||
STDMETHOD(EnumerateLoadedRuntimes)(HANDLE, void**) PURE;
|
||||
STDMETHOD(RequestRuntimeLoadedNotification)(void*) PURE;
|
||||
STDMETHOD(QueryLegacyV2RuntimeBinding)(REFIID, LPVOID*) PURE;
|
||||
STDMETHOD_(void, ExitProcess)(INT32) PURE;
|
||||
};
|
||||
#undef INTERFACE
|
||||
|
||||
#define INTERFACE ICLRRuntimeInfo
|
||||
DECLARE_INTERFACE_(ICLRRuntimeInfo, IUnknown)
|
||||
{
|
||||
// IUnknown
|
||||
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||
STDMETHOD_(ULONG, Release)() PURE;
|
||||
// ICLRRuntimeInfo
|
||||
STDMETHOD(GetVersionString)(LPWSTR, DWORD*) PURE;
|
||||
STDMETHOD(GetRuntimeDirectory)(LPWSTR, DWORD*) PURE;
|
||||
STDMETHOD(IsLoaded)(HANDLE, BOOL*) PURE;
|
||||
STDMETHOD(LoadErrorString)(UINT, LPWSTR, DWORD*, LONG) PURE;
|
||||
STDMETHOD(LoadLibrary)(LPCWSTR, HMODULE*) PURE;
|
||||
STDMETHOD(GetProcAddress)(LPCSTR, LPVOID*) PURE;
|
||||
STDMETHOD(GetInterface)(REFCLSID rclsid, REFIID riid, LPVOID* ppUnk) PURE;
|
||||
};
|
||||
#undef INTERFACE
|
||||
|
||||
#define INTERFACE ICLRRuntimeHost
|
||||
DECLARE_INTERFACE_(ICLRRuntimeHost, IUnknown)
|
||||
{
|
||||
// IUnknown
|
||||
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||
STDMETHOD_(ULONG, Release)() PURE;
|
||||
// ICLRRuntimeHost
|
||||
STDMETHOD(Start)() PURE;
|
||||
STDMETHOD(Stop)() PURE;
|
||||
STDMETHOD(SetHostControl)(void*) PURE;
|
||||
STDMETHOD(GetCLRControl)(void**) PURE;
|
||||
STDMETHOD(UnloadAppDomain)(DWORD, BOOL) PURE;
|
||||
STDMETHOD(ExecuteInAppDomain)(DWORD, void*, void*) PURE;
|
||||
STDMETHOD(GetCurrentAppDomainId)(DWORD*) PURE;
|
||||
STDMETHOD(ExecuteApplication)(LPCWSTR, DWORD, LPCWSTR*, DWORD, LPCWSTR*, int*) PURE;
|
||||
STDMETHOD(ExecuteInDefaultAppDomain)(LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, DWORD*) PURE;
|
||||
};
|
||||
#undef INTERFACE
|
||||
|
||||
// -- CLR Host wrapper -----------------------------------------------------
|
||||
|
||||
class ClrHost
|
||||
{
|
||||
public:
|
||||
ClrHost();
|
||||
~ClrHost();
|
||||
|
||||
// True if the .NET Framework CLR (v4.0) is available on this machine.
|
||||
bool isAvailable() const { return m_runtimeHost != nullptr && m_clrStarted; }
|
||||
|
||||
// Load a managed ReClass.NET plugin via the C# bridge.
|
||||
bool loadManagedPlugin(const QString& bridgeDllPath,
|
||||
const QString& pluginPath,
|
||||
RcNetFunctions* outFunctions,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
private:
|
||||
bool startClr();
|
||||
|
||||
HMODULE m_mscoree = nullptr;
|
||||
ICLRMetaHost* m_metaHost = nullptr;
|
||||
ICLRRuntimeInfo* m_runtimeInfo = nullptr;
|
||||
ICLRRuntimeHost* m_runtimeHost = nullptr;
|
||||
bool m_clrStarted = false;
|
||||
};
|
||||
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal file
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal file
@@ -0,0 +1,333 @@
|
||||
#include "RcNetCompatPlugin.h"
|
||||
#include "RcNetCompatProvider.h"
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QStyle>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// -- Helpers --------------------------------------------------------------
|
||||
|
||||
QIcon RcNetCompatPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_TrashIcon);
|
||||
}
|
||||
|
||||
// --.NET assembly detection ----------------------------------------------
|
||||
|
||||
static bool isDotNetAssembly(const QString& path)
|
||||
{
|
||||
// A .NET assembly has a non-zero CLR header directory entry in the PE
|
||||
// optional header. We check this by loading the PE without running
|
||||
// DllMain and inspecting the IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR.
|
||||
HMODULE hMod = GetModuleHandleW(reinterpret_cast<LPCWSTR>(path.utf16()));
|
||||
if (!hMod)
|
||||
hMod = LoadLibraryExW(reinterpret_cast<LPCWSTR>(path.utf16()),
|
||||
nullptr, DONT_RESOLVE_DLL_REFERENCES);
|
||||
if (!hMod) return false;
|
||||
|
||||
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(hMod);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
|
||||
|
||||
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
|
||||
reinterpret_cast<const char*>(hMod) + dos->e_lfanew);
|
||||
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
|
||||
|
||||
constexpr DWORD kClrIndex = IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR; // 14
|
||||
DWORD rva = 0, dirSize = 0;
|
||||
|
||||
if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
|
||||
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER64*>(&nt->OptionalHeader);
|
||||
if (opt->NumberOfRvaAndSizes > kClrIndex) {
|
||||
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
|
||||
dirSize = opt->DataDirectory[kClrIndex].Size;
|
||||
}
|
||||
} else {
|
||||
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER32*>(&nt->OptionalHeader);
|
||||
if (opt->NumberOfRvaAndSizes > kClrIndex) {
|
||||
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
|
||||
dirSize = opt->DataDirectory[kClrIndex].Size;
|
||||
}
|
||||
}
|
||||
|
||||
return rva != 0 && dirSize != 0;
|
||||
}
|
||||
|
||||
// --Unified loader (dispatches native vs managed) ------------------------
|
||||
|
||||
bool RcNetCompatPlugin::loadPlugin(const QString& path, QString* errorMsg)
|
||||
{
|
||||
if (m_dllPath == path && (m_lib || m_isManaged))
|
||||
return true; // Already loaded
|
||||
|
||||
if (isDotNetAssembly(path)) {
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
return loadManagedDll(path, errorMsg);
|
||||
#else
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"This is a .NET assembly.\n\n"
|
||||
"This build does not include .NET bridge support.\n"
|
||||
"Rebuild with the .NET SDK installed to enable managed plugin loading.");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
return loadNativeDll(path, errorMsg);
|
||||
}
|
||||
|
||||
// --Native DLL loading ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatPlugin::loadNativeDll(const QString& path, QString* errorMsg)
|
||||
{
|
||||
unloadNativeDll();
|
||||
|
||||
m_lib = std::make_unique<QLibrary>(path);
|
||||
if (!m_lib->load()) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to load DLL: %1").arg(m_lib->errorString());
|
||||
m_lib.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve all function pointers
|
||||
m_fns.EnumerateProcesses =
|
||||
reinterpret_cast<FnEnumerateProcesses>(m_lib->resolve("EnumerateProcesses"));
|
||||
m_fns.OpenRemoteProcess =
|
||||
reinterpret_cast<FnOpenRemoteProcess>(m_lib->resolve("OpenRemoteProcess"));
|
||||
m_fns.IsProcessValid =
|
||||
reinterpret_cast<FnIsProcessValid>(m_lib->resolve("IsProcessValid"));
|
||||
m_fns.CloseRemoteProcess =
|
||||
reinterpret_cast<FnCloseRemoteProcess>(m_lib->resolve("CloseRemoteProcess"));
|
||||
m_fns.ReadRemoteMemory =
|
||||
reinterpret_cast<FnReadRemoteMemory>(m_lib->resolve("ReadRemoteMemory"));
|
||||
m_fns.WriteRemoteMemory =
|
||||
reinterpret_cast<FnWriteRemoteMemory>(m_lib->resolve("WriteRemoteMemory"));
|
||||
m_fns.EnumerateRemoteSectionsAndModules =
|
||||
reinterpret_cast<FnEnumerateRemoteSectionsAndModules>(
|
||||
m_lib->resolve("EnumerateRemoteSectionsAndModules"));
|
||||
m_fns.ControlRemoteProcess =
|
||||
reinterpret_cast<FnControlRemoteProcess>(m_lib->resolve("ControlRemoteProcess"));
|
||||
|
||||
// At minimum we need read + open + close
|
||||
if (!m_fns.ReadRemoteMemory || !m_fns.OpenRemoteProcess || !m_fns.CloseRemoteProcess || !m_fns.EnumerateProcesses) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"DLL is missing required exports (ReadRemoteMemory, OpenRemoteProcess, "
|
||||
"CloseRemoteProcess, EnumerateProcesses). Is this a ReClass.NET native plugin?");
|
||||
m_lib->unload();
|
||||
m_lib.reset();
|
||||
m_fns = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
m_dllPath = path;
|
||||
m_isManaged = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RcNetCompatPlugin::unloadNativeDll()
|
||||
{
|
||||
if (m_lib) {
|
||||
m_lib->unload();
|
||||
m_lib.reset();
|
||||
}
|
||||
m_fns = {};
|
||||
m_dllPath.clear();
|
||||
m_isManaged = false;
|
||||
}
|
||||
|
||||
// --Managed (.NET) DLL loading via CLR bridge ----------------------------
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
|
||||
bool RcNetCompatPlugin::loadManagedDll(const QString& path, QString* errorMsg)
|
||||
{
|
||||
unloadNativeDll();
|
||||
|
||||
// Lazily create the CLR host (one per plugin lifetime)
|
||||
if (!m_clrHost)
|
||||
m_clrHost = std::make_unique<ClrHost>();
|
||||
|
||||
if (!m_clrHost->isAvailable()) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
".NET Framework 4.x is not available on this machine.\n"
|
||||
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Locate RcNetBridge.dll next to our own plugin DLL
|
||||
// Use native separators -- the CLR expects Windows-style backslash paths.
|
||||
QString bridgePath = QDir::toNativeSeparators(
|
||||
QCoreApplication::applicationDirPath()
|
||||
+ QStringLiteral("/Plugins/RcNetBridge.dll"));
|
||||
|
||||
if (!QFileInfo::exists(bridgePath)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"RcNetBridge.dll not found in the Plugins folder.\n"
|
||||
"Expected at: %1").arg(bridgePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_fns = {};
|
||||
QString nativePath = QDir::toNativeSeparators(path);
|
||||
if (!m_clrHost->loadManagedPlugin(bridgePath, nativePath, &m_fns, errorMsg))
|
||||
return false;
|
||||
|
||||
m_dllPath = path;
|
||||
m_isManaged = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // HAS_CLR_BRIDGE
|
||||
|
||||
// --IProviderPlugin ------------------------------------------------------
|
||||
|
||||
bool RcNetCompatPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
// Target format: "dllpath|pid:name"
|
||||
return target.contains('|');
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider> RcNetCompatPlugin::createProvider(
|
||||
const QString& target, QString* errorMsg)
|
||||
{
|
||||
// Parse "dllpath|pid:name"
|
||||
int sep = target.indexOf('|');
|
||||
if (sep < 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid target format");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString dllPath = target.left(sep);
|
||||
QString pidPart = target.mid(sep + 1);
|
||||
|
||||
// Load (or reuse) the plugin DLL
|
||||
if (!loadPlugin(dllPath, errorMsg))
|
||||
return nullptr;
|
||||
|
||||
// Parse pid:name
|
||||
QStringList parts = pidPart.split(':');
|
||||
bool ok = false;
|
||||
uint32_t pid = parts[0].toUInt(&ok);
|
||||
if (!ok || pid == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID: %1").arg(parts[0]);
|
||||
return nullptr;
|
||||
}
|
||||
QString procName = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid);
|
||||
|
||||
auto provider = std::make_unique<RcNetCompatProvider>(m_fns, pid, procName);
|
||||
if (!provider->isValid()) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"Failed to open process %1 (PID: %2) via ReClass.NET plugin.\n"
|
||||
"Ensure the process is running and the plugin supports it.")
|
||||
.arg(procName).arg(pid);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
uint64_t RcNetCompatPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
Q_UNUSED(target);
|
||||
// The provider sets its own base from module enumeration.
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool RcNetCompatPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
// Step 1: Pick a ReClass.NET plugin DLL (native or .NET)
|
||||
QString dllPath = QFileDialog::getOpenFileName(
|
||||
parent,
|
||||
QStringLiteral("Select ReClass.NET Plugin"),
|
||||
QString(),
|
||||
QStringLiteral("DLL Files (*.dll)"));
|
||||
|
||||
if (dllPath.isEmpty())
|
||||
return false;
|
||||
|
||||
// Step 2: Load and validate the DLL
|
||||
QString loadErr;
|
||||
if (!loadPlugin(dllPath, &loadErr)) {
|
||||
QMessageBox::warning(parent,
|
||||
QStringLiteral("ReClass.NET Compat Layer"),
|
||||
loadErr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Enumerate processes and show picker
|
||||
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||
|
||||
QList<ProcessInfo> processes;
|
||||
for (const auto& p : pluginProcesses) {
|
||||
ProcessInfo info;
|
||||
info.pid = p.pid;
|
||||
info.name = p.name;
|
||||
info.path = p.path;
|
||||
info.icon = p.icon;
|
||||
processes.append(info);
|
||||
}
|
||||
|
||||
ProcessPicker picker(processes, parent);
|
||||
if (picker.exec() != QDialog::Accepted)
|
||||
return false;
|
||||
|
||||
uint32_t pid = picker.selectedProcessId();
|
||||
QString name = picker.selectedProcessName();
|
||||
|
||||
// Step 4: Format target as "dllpath|pid:name"
|
||||
*target = QStringLiteral("%1|%2:%3").arg(dllPath).arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --Process enumeration --------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
struct ProcessCollector {
|
||||
QVector<PluginProcessInfo>* dest = nullptr;
|
||||
};
|
||||
thread_local ProcessCollector g_processCollector;
|
||||
|
||||
void RC_CALLCONV processCallback(EnumerateProcessData* data)
|
||||
{
|
||||
if (!data || !g_processCollector.dest) return;
|
||||
|
||||
PluginProcessInfo info;
|
||||
info.pid = static_cast<uint32_t>(data->Id);
|
||||
info.name = QString::fromUtf16(data->Name);
|
||||
info.path = QString::fromUtf16(data->Path);
|
||||
g_processCollector.dest->append(info);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
QVector<PluginProcessInfo> RcNetCompatPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> result;
|
||||
|
||||
if (!m_fns.EnumerateProcesses)
|
||||
return result;
|
||||
|
||||
g_processCollector.dest = &result;
|
||||
m_fns.EnumerateProcesses(processCallback);
|
||||
g_processCollector.dest = nullptr;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// --Plugin factory -------------------------------------------------------
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new RcNetCompatPlugin();
|
||||
}
|
||||
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal file
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
|
||||
#include <QLibrary>
|
||||
#include <memory>
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
#include "ClrHost.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* ReclassX plugin that loads ReClass.NET plugin DLLs
|
||||
* and exposes them as ReclassX providers.
|
||||
*
|
||||
* Supports both native DLLs (C exports) and, when built with
|
||||
* HAS_CLR_BRIDGE, managed .NET assemblies via in-process CLR hosting.
|
||||
*
|
||||
* Target string format: "dllpath|pid:processname"
|
||||
*/
|
||||
class RcNetCompatPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
// Plugin metadata
|
||||
std::string Name() const override { return "ReClass.NET Compat Layer"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override {
|
||||
return "Loads ReClass.NET native and .NET plugin DLLs as Reclass data sources";
|
||||
}
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
// IProviderPlugin interface
|
||||
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;
|
||||
|
||||
// Override process enumeration -- we enumerate via the loaded DLL
|
||||
bool providesProcessList() const override { return true; }
|
||||
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||
|
||||
private:
|
||||
bool loadPlugin(const QString& path, QString* errorMsg = nullptr);
|
||||
bool loadNativeDll(const QString& path, QString* errorMsg = nullptr);
|
||||
void unloadNativeDll();
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
bool loadManagedDll(const QString& path, QString* errorMsg = nullptr);
|
||||
std::unique_ptr<ClrHost> m_clrHost;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<QLibrary> m_lib;
|
||||
RcNetFunctions m_fns;
|
||||
QString m_dllPath;
|
||||
bool m_isManaged = false;
|
||||
};
|
||||
|
||||
// Plugin export
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
132
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
132
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "RcNetCompatProvider.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <cstring>
|
||||
|
||||
// -- Construction / destruction -------------------------------------------
|
||||
|
||||
RcNetCompatProvider::RcNetCompatProvider(const RcNetFunctions& fns,
|
||||
uint32_t pid,
|
||||
const QString& processName)
|
||||
: m_fns(fns)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
{
|
||||
if (m_fns.OpenRemoteProcess)
|
||||
m_handle = m_fns.OpenRemoteProcess(static_cast<RC_Size>(pid),
|
||||
ProcessAccess::Full);
|
||||
|
||||
if (m_handle)
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
RcNetCompatProvider::~RcNetCompatProvider()
|
||||
{
|
||||
if (m_handle && m_fns.CloseRemoteProcess)
|
||||
m_fns.CloseRemoteProcess(m_handle);
|
||||
}
|
||||
|
||||
// -- Required overrides ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
|
||||
return false;
|
||||
|
||||
return m_fns.ReadRemoteMemory(m_handle,
|
||||
reinterpret_cast<RC_Pointer>(addr),
|
||||
static_cast<RC_Pointer>(buf),
|
||||
0, len);
|
||||
}
|
||||
|
||||
int RcNetCompatProvider::size() const
|
||||
{
|
||||
if (!m_handle) return 0;
|
||||
if (m_fns.IsProcessValid && !m_fns.IsProcessValid(m_handle)) return 0;
|
||||
return 0x10000;
|
||||
}
|
||||
|
||||
// -- Optional overrides ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
|
||||
return false;
|
||||
|
||||
return m_fns.WriteRemoteMemory(m_handle,
|
||||
reinterpret_cast<RC_Pointer>(addr),
|
||||
const_cast<RC_Pointer>(static_cast<const void*>(buf)),
|
||||
0, len);
|
||||
}
|
||||
|
||||
QString RcNetCompatProvider::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 RcNetCompatProvider::symbolToAddress(const QString& name) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
|
||||
return mod.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -- Module enumeration ---------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Thread-local collector for the module enumeration callback.
|
||||
// ReClass.NET callbacks are synchronous, so this is safe.
|
||||
struct ModuleCollector {
|
||||
QVector<RcNetCompatProvider::ModuleInfo>* dest = nullptr;
|
||||
};
|
||||
thread_local ModuleCollector g_moduleCollector;
|
||||
|
||||
void RC_CALLCONV moduleCallback(EnumerateRemoteModuleData* data)
|
||||
{
|
||||
if (!data || !g_moduleCollector.dest) return;
|
||||
|
||||
QString path = QString::fromUtf16(data->Path);
|
||||
QFileInfo fi(path);
|
||||
|
||||
RcNetCompatProvider::ModuleInfo info;
|
||||
info.name = fi.fileName();
|
||||
info.base = reinterpret_cast<uint64_t>(data->BaseAddress);
|
||||
info.size = static_cast<uint64_t>(data->Size);
|
||||
g_moduleCollector.dest->append(info);
|
||||
}
|
||||
|
||||
// We still need a section callback even though we don't use it.
|
||||
void RC_CALLCONV sectionCallback(EnumerateRemoteSectionData*)
|
||||
{
|
||||
// Intentionally empty -- we only need module data.
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void RcNetCompatProvider::cacheModules()
|
||||
{
|
||||
if (!m_fns.EnumerateRemoteSectionsAndModules || !m_handle)
|
||||
return;
|
||||
|
||||
m_modules.clear();
|
||||
g_moduleCollector.dest = &m_modules;
|
||||
m_fns.EnumerateRemoteSectionsAndModules(m_handle, sectionCallback, moduleCallback);
|
||||
g_moduleCollector.dest = nullptr;
|
||||
|
||||
// Set base to first module if we got any
|
||||
if (!m_modules.isEmpty() && m_base == 0)
|
||||
m_base = m_modules.first().base;
|
||||
}
|
||||
48
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
48
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "../../src/providers/provider.h"
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
* Provider that bridges ReClass.NET native plugin DLL calls
|
||||
* to the ReclassX Provider interface.
|
||||
*/
|
||||
class RcNetCompatProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
RcNetCompatProvider(const RcNetFunctions& fns, uint32_t pid,
|
||||
const QString& processName);
|
||||
~RcNetCompatProvider() override;
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
// Optional overrides
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_fns.WriteRemoteMemory != nullptr; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("RcNet"); }
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
uint64_t symbolToAddress(const QString& name) const override;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
};
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
|
||||
RcNetFunctions m_fns;
|
||||
RC_Pointer m_handle = nullptr;
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
uint64_t m_base = 0;
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
// Subset of ReClass.NET native plugin types needed for the compatibility layer.
|
||||
// Based on the ReClass.NET NativeCore plugin interface.
|
||||
// Only types required by the 8 supported exports are included (no debug types).
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define RC_CALLCONV __stdcall
|
||||
#else
|
||||
#define RC_CALLCONV
|
||||
#endif
|
||||
|
||||
// -- Basic types ----------------------------------------------------------
|
||||
|
||||
using RC_Pointer = void*;
|
||||
using RC_Size = uint64_t;
|
||||
using RC_UnicodeChar = char16_t;
|
||||
|
||||
// -- Enums ----------------------------------------------------------------
|
||||
|
||||
enum class ProcessAccess
|
||||
{
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Full = 2
|
||||
};
|
||||
|
||||
enum class SectionProtection
|
||||
{
|
||||
NoAccess = 0,
|
||||
Read = 1,
|
||||
Write = 2,
|
||||
Execute = 4,
|
||||
Guard = 8
|
||||
};
|
||||
|
||||
enum class SectionType
|
||||
{
|
||||
Unknown = 0,
|
||||
Private = 1,
|
||||
Mapped = 2,
|
||||
Image = 3
|
||||
};
|
||||
|
||||
enum class SectionCategory
|
||||
{
|
||||
Unknown = 0,
|
||||
CODE = 1,
|
||||
DATA = 2,
|
||||
HEAP = 3
|
||||
};
|
||||
|
||||
enum class ControlRemoteProcessAction
|
||||
{
|
||||
Suspend = 0,
|
||||
Resume = 1,
|
||||
Terminate = 2
|
||||
};
|
||||
|
||||
// -- Callback data structures ---------------------------------------------
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct EnumerateProcessData
|
||||
{
|
||||
RC_Size Id;
|
||||
RC_UnicodeChar Name[260];
|
||||
RC_UnicodeChar Path[260];
|
||||
};
|
||||
|
||||
struct EnumerateRemoteSectionData
|
||||
{
|
||||
RC_Pointer BaseAddress;
|
||||
RC_Size Size;
|
||||
SectionType Type;
|
||||
SectionCategory Category;
|
||||
SectionProtection Protection;
|
||||
RC_UnicodeChar Name[16];
|
||||
RC_UnicodeChar ModulePath[260];
|
||||
};
|
||||
|
||||
struct EnumerateRemoteModuleData
|
||||
{
|
||||
RC_Pointer BaseAddress;
|
||||
RC_Size Size;
|
||||
RC_UnicodeChar Path[260];
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
// -- Callback typedefs ----------------------------------------------------
|
||||
|
||||
using EnumerateProcessCallback = void(RC_CALLCONV*)(EnumerateProcessData* data);
|
||||
using EnumerateRemoteSectionsCallback = void(RC_CALLCONV*)(EnumerateRemoteSectionData* data);
|
||||
using EnumerateRemoteModulesCallback = void(RC_CALLCONV*)(EnumerateRemoteModuleData* data);
|
||||
|
||||
// -- Function pointer typedefs for resolved exports -----------------------
|
||||
|
||||
using FnEnumerateProcesses = void(RC_CALLCONV*)(EnumerateProcessCallback callback);
|
||||
|
||||
using FnOpenRemoteProcess = RC_Pointer(RC_CALLCONV*)(RC_Size id, ProcessAccess desiredAccess);
|
||||
|
||||
using FnIsProcessValid = bool(RC_CALLCONV*)(RC_Pointer handle);
|
||||
|
||||
using FnCloseRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle);
|
||||
|
||||
using FnReadRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
|
||||
RC_Pointer address,
|
||||
RC_Pointer buffer,
|
||||
int offset,
|
||||
int size);
|
||||
|
||||
using FnWriteRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
|
||||
RC_Pointer address,
|
||||
RC_Pointer buffer,
|
||||
int offset,
|
||||
int size);
|
||||
|
||||
using FnEnumerateRemoteSectionsAndModules =
|
||||
void(RC_CALLCONV*)(RC_Pointer handle,
|
||||
EnumerateRemoteSectionsCallback sectionCallback,
|
||||
EnumerateRemoteModulesCallback moduleCallback);
|
||||
|
||||
using FnControlRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle,
|
||||
ControlRemoteProcessAction action);
|
||||
|
||||
// -- Resolved function table ----------------------------------------------
|
||||
|
||||
struct RcNetFunctions
|
||||
{
|
||||
FnEnumerateProcesses EnumerateProcesses = nullptr;
|
||||
FnOpenRemoteProcess OpenRemoteProcess = nullptr;
|
||||
FnIsProcessValid IsProcessValid = nullptr;
|
||||
FnCloseRemoteProcess CloseRemoteProcess = nullptr;
|
||||
FnReadRemoteMemory ReadRemoteMemory = nullptr;
|
||||
FnWriteRemoteMemory WriteRemoteMemory = nullptr;
|
||||
FnEnumerateRemoteSectionsAndModules EnumerateRemoteSectionsAndModules = nullptr;
|
||||
FnControlRemoteProcess ControlRemoteProcess = nullptr;
|
||||
};
|
||||
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal file
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal file
@@ -0,0 +1,677 @@
|
||||
// RcNetBridge -- in-process C# bridge for loading .NET ReClass.NET plugins.
|
||||
//
|
||||
// Called from C++ via ICLRRuntimeHost::ExecuteInDefaultAppDomain().
|
||||
// The single entry point is Bridge.Initialize(string arg) where arg is:
|
||||
// "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
|
||||
//
|
||||
// The bridge:
|
||||
// 1. Registers an AssemblyResolve handler that provides THIS assembly
|
||||
// when a plugin asks for "ReClassNET", so the stub types below satisfy
|
||||
// the plugin's type references.
|
||||
// 2. Loads the plugin assembly and finds an ICoreProcessFunctions
|
||||
// implementation.
|
||||
// 3. Creates [UnmanagedFunctionPointer] delegates wrapping each method.
|
||||
// 4. Writes the native-callable function pointers into the RcNetFunctions
|
||||
// struct at the address provided by C++.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// ===========================================================================
|
||||
// ReClass.NET stub types
|
||||
// These mirror the subset of types from the ReClass.NET assembly that
|
||||
// memory-reading plugins reference. When the CLR resolves "ReClassNET"
|
||||
// via our AssemblyResolve handler, it gets THIS assembly, and these types
|
||||
// satisfy the plugin's type references.
|
||||
//
|
||||
// Types are placed in the exact namespaces used by the real ReClass.NET
|
||||
// assembly so that plugins compiled against it resolve correctly.
|
||||
// ===========================================================================
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Memory -- section enums (referenced by EnumerateRemoteSectionData)
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Memory
|
||||
{
|
||||
public enum SectionProtection
|
||||
{
|
||||
NoAccess = 0,
|
||||
Read = 1,
|
||||
Write = 2,
|
||||
Execute = 4,
|
||||
Guard = 8
|
||||
}
|
||||
|
||||
public enum SectionType
|
||||
{
|
||||
Unknown = 0,
|
||||
Private = 1,
|
||||
Mapped = 2,
|
||||
Image = 3
|
||||
}
|
||||
|
||||
public enum SectionCategory
|
||||
{
|
||||
Unknown = 0,
|
||||
CODE = 1,
|
||||
DATA = 2,
|
||||
HEAP = 3
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Debugger -- debugger types (used by ICoreProcessFunctions)
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Debugger
|
||||
{
|
||||
public enum DebugContinueStatus
|
||||
{
|
||||
Handled = 0,
|
||||
NotHandled = 1
|
||||
}
|
||||
|
||||
public enum HardwareBreakpointRegister
|
||||
{
|
||||
InvalidRegister = 0,
|
||||
Dr0 = 1,
|
||||
Dr1 = 2,
|
||||
Dr2 = 3,
|
||||
Dr3 = 4
|
||||
}
|
||||
|
||||
public enum HardwareBreakpointTrigger
|
||||
{
|
||||
Execute = 0,
|
||||
Access = 1,
|
||||
Write = 2
|
||||
}
|
||||
|
||||
public enum HardwareBreakpointSize
|
||||
{
|
||||
Size1 = 1,
|
||||
Size2 = 2,
|
||||
Size4 = 4,
|
||||
Size8 = 8
|
||||
}
|
||||
|
||||
public struct ExceptionDebugInfo
|
||||
{
|
||||
public IntPtr ExceptionCode;
|
||||
public IntPtr ExceptionFlags;
|
||||
public IntPtr ExceptionAddress;
|
||||
public HardwareBreakpointRegister CausedBy;
|
||||
public RegisterInfo Registers;
|
||||
|
||||
public struct RegisterInfo
|
||||
{
|
||||
public IntPtr Rax, Rbx, Rcx, Rdx;
|
||||
public IntPtr Rdi, Rsi, Rsp, Rbp, Rip;
|
||||
public IntPtr R8, R9, R10, R11, R12, R13, R14, R15;
|
||||
}
|
||||
}
|
||||
|
||||
public struct DebugEvent
|
||||
{
|
||||
public DebugContinueStatus ContinueStatus;
|
||||
public IntPtr ProcessId;
|
||||
public IntPtr ThreadId;
|
||||
public ExceptionDebugInfo ExceptionInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Core -- interface, enums, delegates, and data structs
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Core
|
||||
{
|
||||
public enum ProcessAccess
|
||||
{
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Full = 2
|
||||
}
|
||||
|
||||
public enum ControlRemoteProcessAction
|
||||
{
|
||||
Suspend = 0,
|
||||
Resume = 1,
|
||||
Terminate = 2
|
||||
}
|
||||
|
||||
public struct EnumerateProcessData
|
||||
{
|
||||
public IntPtr Id;
|
||||
public string Name;
|
||||
public string Path;
|
||||
}
|
||||
|
||||
public struct EnumerateRemoteSectionData
|
||||
{
|
||||
public IntPtr BaseAddress;
|
||||
public IntPtr Size;
|
||||
public ReClassNET.Memory.SectionType Type;
|
||||
public ReClassNET.Memory.SectionCategory Category;
|
||||
public ReClassNET.Memory.SectionProtection Protection;
|
||||
public string Name;
|
||||
public string ModulePath;
|
||||
}
|
||||
|
||||
public struct EnumerateRemoteModuleData
|
||||
{
|
||||
public IntPtr BaseAddress;
|
||||
public IntPtr Size;
|
||||
public string Path;
|
||||
}
|
||||
|
||||
public delegate void EnumerateProcessCallback(ref EnumerateProcessData data);
|
||||
public delegate void EnumerateRemoteSectionCallback(ref EnumerateRemoteSectionData data);
|
||||
public delegate void EnumerateRemoteModuleCallback(ref EnumerateRemoteModuleData data);
|
||||
|
||||
public interface ICoreProcessFunctions
|
||||
{
|
||||
void EnumerateProcesses(EnumerateProcessCallback callbackProcess);
|
||||
IntPtr OpenRemoteProcess(IntPtr pid, ProcessAccess desiredAccess);
|
||||
bool IsProcessValid(IntPtr process);
|
||||
void CloseRemoteProcess(IntPtr process);
|
||||
bool ReadRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
|
||||
bool WriteRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
|
||||
void EnumerateRemoteSectionsAndModules(
|
||||
IntPtr process,
|
||||
EnumerateRemoteSectionCallback callbackSection,
|
||||
EnumerateRemoteModuleCallback callbackModule);
|
||||
void ControlRemoteProcess(IntPtr process, ControlRemoteProcessAction action);
|
||||
|
||||
// Debugger methods -- stubs required for interface compatibility
|
||||
bool AttachDebuggerToProcess(IntPtr id);
|
||||
void DetachDebuggerFromProcess(IntPtr id);
|
||||
bool AwaitDebugEvent(ref ReClassNET.Debugger.DebugEvent evt, int timeoutInMilliseconds);
|
||||
void HandleDebugEvent(ref ReClassNET.Debugger.DebugEvent evt);
|
||||
bool SetHardwareBreakpoint(IntPtr id, IntPtr address,
|
||||
ReClassNET.Debugger.HardwareBreakpointRegister register,
|
||||
ReClassNET.Debugger.HardwareBreakpointTrigger trigger,
|
||||
ReClassNET.Debugger.HardwareBreakpointSize size,
|
||||
bool set);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Memory -- RemoteProcess stub
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Memory
|
||||
{
|
||||
public class RemoteProcess { }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Logger -- ILogger stub
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Logger
|
||||
{
|
||||
public interface ILogger { }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Stub types for IPluginHost properties
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Forms
|
||||
{
|
||||
public class MainForm { }
|
||||
}
|
||||
|
||||
namespace ReClassNET
|
||||
{
|
||||
public class Settings { }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Plugins
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Plugins
|
||||
{
|
||||
public abstract class Plugin : IDisposable
|
||||
{
|
||||
public virtual bool Initialize(IPluginHost host) { return true; }
|
||||
public virtual void Terminate() { }
|
||||
public virtual void Dispose() { }
|
||||
}
|
||||
|
||||
public interface IPluginHost
|
||||
{
|
||||
ReClassNET.Forms.MainForm MainWindow { get; }
|
||||
System.Resources.ResourceManager Resources { get; }
|
||||
ReClassNET.Memory.RemoteProcess Process { get; }
|
||||
ReClassNET.Logger.ILogger Logger { get; }
|
||||
ReClassNET.Settings Settings { get; }
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Bridge
|
||||
// ===========================================================================
|
||||
|
||||
namespace RcNetBridge
|
||||
{
|
||||
internal class StubPluginHost : ReClassNET.Plugins.IPluginHost
|
||||
{
|
||||
public ReClassNET.Forms.MainForm MainWindow => null;
|
||||
public System.Resources.ResourceManager Resources => null;
|
||||
public ReClassNET.Memory.RemoteProcess Process => null;
|
||||
public ReClassNET.Logger.ILogger Logger => null;
|
||||
public ReClassNET.Settings Settings => null;
|
||||
}
|
||||
|
||||
public class Bridge
|
||||
{
|
||||
// -- Persistent state (static so it survives after Initialize returns) --
|
||||
|
||||
private static ReClassNET.Core.ICoreProcessFunctions s_functions;
|
||||
private static readonly List<Delegate> s_pinned = new List<Delegate>();
|
||||
|
||||
// -- Entry point called from C++ --------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Called by ICLRRuntimeHost::ExecuteInDefaultAppDomain.
|
||||
/// arg = "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
|
||||
/// Returns 0 on success, non-zero error code on failure.
|
||||
/// </summary>
|
||||
public static int Initialize(string arg)
|
||||
{
|
||||
try
|
||||
{
|
||||
int sep = arg.IndexOf('|');
|
||||
if (sep < 0) return 1; // bad arg
|
||||
|
||||
long ptrValue = long.Parse(arg.Substring(0, sep), NumberStyles.HexNumber);
|
||||
IntPtr funcTablePtr = new IntPtr(ptrValue);
|
||||
string pluginPath = arg.Substring(sep + 1);
|
||||
|
||||
// Set up assembly resolution
|
||||
string pluginDir = Path.GetDirectoryName(pluginPath) ?? ".";
|
||||
string parentDir = Path.GetDirectoryName(pluginDir);
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
|
||||
{
|
||||
string asmName = new AssemblyName(resolveArgs.Name).Name;
|
||||
|
||||
// Provide our own assembly as the "ReClass.NET" stub
|
||||
if (string.Equals(asmName, "ReClass.NET", StringComparison.OrdinalIgnoreCase))
|
||||
return typeof(Bridge).Assembly;
|
||||
|
||||
// Search plugin directory and parent for other dependencies
|
||||
string dllName = asmName + ".dll";
|
||||
foreach (string dir in new[] { pluginDir, parentDir })
|
||||
{
|
||||
if (dir == null) continue;
|
||||
string path = Path.Combine(dir, dllName);
|
||||
if (File.Exists(path))
|
||||
return Assembly.LoadFrom(path);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Load plugin and find ICoreProcessFunctions
|
||||
if (!LoadPlugin(pluginPath))
|
||||
return 2; // no implementation found
|
||||
|
||||
// Write function pointers
|
||||
WriteFunctionPointers(funcTablePtr);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) when (ex is ReflectionTypeLoadException || ex is FileNotFoundException)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Plugin loading ---------------------------------------------------
|
||||
|
||||
private static bool LoadPlugin(string pluginPath)
|
||||
{
|
||||
Assembly asm = Assembly.LoadFrom(pluginPath);
|
||||
|
||||
// Find a concrete type that implements ICoreProcessFunctions.
|
||||
// ReClass.NET plugins typically extend Plugin and directly
|
||||
// implement ICoreProcessFunctions on the same class.
|
||||
foreach (Type type in asm.GetExportedTypes())
|
||||
{
|
||||
if (type.IsAbstract || type.IsInterface) continue;
|
||||
|
||||
Type iface = type.GetInterfaces().FirstOrDefault(i =>
|
||||
i.FullName == "ReClassNET.Core.ICoreProcessFunctions");
|
||||
if (iface == null) continue;
|
||||
|
||||
object instance = Activator.CreateInstance(type);
|
||||
|
||||
// Try calling Initialize() but don't fail if it throws --
|
||||
// plugins use it for UI integration with the host app,
|
||||
// which we can't fully provide. The process functions
|
||||
// (ReadRemoteMemory, etc.) work without it.
|
||||
try
|
||||
{
|
||||
MethodInfo init = type.GetMethod("Initialize",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null, new[] { typeof(ReClassNET.Plugins.IPluginHost) }, null);
|
||||
if (init != null)
|
||||
init.Invoke(instance, new object[] { new StubPluginHost() });
|
||||
}
|
||||
catch { }
|
||||
|
||||
s_functions = (ReClassNET.Core.ICoreProcessFunctions)instance;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// -- Native-callable delegate types -----------------------------------
|
||||
// These match the C++ RcNetFunctions struct field order exactly.
|
||||
// On x64 Windows all calling conventions collapse to the Microsoft
|
||||
// x64 ABI, so StdCall is used for documentation / x86 correctness.
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelEnumProcesses(IntPtr callback);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate IntPtr DelOpenRemoteProcess(ulong id, int access);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
delegate bool DelIsProcessValid(IntPtr handle);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelCloseRemoteProcess(IntPtr handle);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
delegate bool DelReadRemoteMemory(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
delegate bool DelWriteRemoteMemory(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelEnumSectionsAndModules(IntPtr handle,
|
||||
IntPtr sectionCallback, IntPtr moduleCallback);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelControlRemoteProcess(IntPtr handle, int action);
|
||||
|
||||
// Callback delegate types -- these point into C++ and are called by us.
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void NativeProcessCallback(IntPtr data);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void NativeSectionCallback(IntPtr data);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void NativeModuleCallback(IntPtr data);
|
||||
|
||||
// -- Write function pointers to the C++ struct ------------------------
|
||||
|
||||
private static void WriteFunctionPointers(IntPtr funcTable)
|
||||
{
|
||||
// RcNetFunctions layout: 8 consecutive function pointers.
|
||||
int i = 0;
|
||||
WriteSlot(funcTable, i++, Pin<DelEnumProcesses>(EnumProcessesImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelOpenRemoteProcess>(OpenProcessImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelIsProcessValid>(IsProcessValidImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelCloseRemoteProcess>(CloseProcessImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelReadRemoteMemory>(ReadMemoryImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelWriteRemoteMemory>(WriteMemoryImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelEnumSectionsAndModules>(EnumSectionsModulesImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelControlRemoteProcess>(ControlProcessImpl));
|
||||
}
|
||||
|
||||
private static IntPtr Pin<T>(T del) where T : class
|
||||
{
|
||||
Delegate d = del as Delegate;
|
||||
s_pinned.Add(d); // prevent GC
|
||||
return Marshal.GetFunctionPointerForDelegate(d);
|
||||
}
|
||||
|
||||
private static void WriteSlot(IntPtr table, int index, IntPtr value)
|
||||
{
|
||||
Marshal.WriteIntPtr(table, index * IntPtr.Size, value);
|
||||
}
|
||||
|
||||
// -- Implementation methods -------------------------------------------
|
||||
|
||||
// -- EnumerateProcesses --
|
||||
// C++ passes a native callback; we call the plugin, convert each
|
||||
// managed EnumerateProcessData to the packed native layout, and
|
||||
// forward to the native callback.
|
||||
|
||||
private static void EnumProcessesImpl(IntPtr nativeCallbackPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null || nativeCallbackPtr == IntPtr.Zero) return;
|
||||
|
||||
NativeProcessCallback nativeCb =
|
||||
Marshal.GetDelegateForFunctionPointer<NativeProcessCallback>(nativeCallbackPtr);
|
||||
|
||||
// Native layout (pack=1): uint64 Id + char16[260] Name + char16[260] Path
|
||||
const int kStructSize = 8 + 520 + 520; // 1048 bytes
|
||||
|
||||
s_functions.EnumerateProcesses(
|
||||
(ref ReClassNET.Core.EnumerateProcessData data) =>
|
||||
{
|
||||
IntPtr mem = Marshal.AllocHGlobal(kStructSize);
|
||||
try
|
||||
{
|
||||
// Zero-fill
|
||||
byte[] zeros = new byte[kStructSize];
|
||||
Marshal.Copy(zeros, 0, mem, kStructSize);
|
||||
|
||||
// Id (8 bytes at offset 0)
|
||||
Marshal.WriteInt64(mem, 0, data.Id.ToInt64());
|
||||
|
||||
// Name (char16[260] at offset 8)
|
||||
if (data.Name != null)
|
||||
{
|
||||
char[] chars = data.Name.ToCharArray();
|
||||
int count = Math.Min(chars.Length, 259);
|
||||
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 8), count);
|
||||
}
|
||||
|
||||
// Path (char16[260] at offset 528)
|
||||
if (data.Path != null)
|
||||
{
|
||||
char[] chars = data.Path.ToCharArray();
|
||||
int count = Math.Min(chars.Length, 259);
|
||||
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 528), count);
|
||||
}
|
||||
|
||||
nativeCb(mem);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(mem);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch { /* swallow -- don't crash the host process */ }
|
||||
}
|
||||
|
||||
// -- OpenRemoteProcess --
|
||||
private static IntPtr OpenProcessImpl(ulong id, int access)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null) return IntPtr.Zero;
|
||||
return s_functions.OpenRemoteProcess(
|
||||
new IntPtr((long)id),
|
||||
(ReClassNET.Core.ProcessAccess)access);
|
||||
}
|
||||
catch { return IntPtr.Zero; }
|
||||
}
|
||||
|
||||
// -- IsProcessValid --
|
||||
private static bool IsProcessValidImpl(IntPtr handle)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null) return false;
|
||||
return s_functions.IsProcessValid(handle);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// -- CloseRemoteProcess --
|
||||
private static void CloseProcessImpl(IntPtr handle)
|
||||
{
|
||||
try { s_functions?.CloseRemoteProcess(handle); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
// -- ReadRemoteMemory --
|
||||
// C++ provides a native buffer pointer. We read into a managed array
|
||||
// via the plugin's interface, then copy to the native buffer.
|
||||
private static bool ReadMemoryImpl(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null || size <= 0) return false;
|
||||
|
||||
byte[] managed = new byte[size];
|
||||
bool ok = s_functions.ReadRemoteMemory(
|
||||
handle, address, ref managed, 0, size);
|
||||
|
||||
if (ok)
|
||||
Marshal.Copy(managed, 0, new IntPtr(buffer.ToInt64() + offset), size);
|
||||
|
||||
return ok;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// -- WriteRemoteMemory --
|
||||
private static bool WriteMemoryImpl(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null || size <= 0) return false;
|
||||
|
||||
byte[] managed = new byte[size];
|
||||
Marshal.Copy(new IntPtr(buffer.ToInt64() + offset), managed, 0, size);
|
||||
|
||||
return s_functions.WriteRemoteMemory(
|
||||
handle, address, ref managed, 0, size);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// -- EnumerateRemoteSectionsAndModules --
|
||||
private static void EnumSectionsModulesImpl(IntPtr handle,
|
||||
IntPtr sectionCallbackPtr, IntPtr moduleCallbackPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null) return;
|
||||
|
||||
// Section callback -- forward to native
|
||||
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
|
||||
// SectionType(4) + SectionCategory(4) + SectionProtection(4) +
|
||||
// char16 Name[16](32) + char16 ModulePath[260](520) = 580 bytes
|
||||
NativeSectionCallback nativeSectionCb = (sectionCallbackPtr != IntPtr.Zero)
|
||||
? Marshal.GetDelegateForFunctionPointer<NativeSectionCallback>(sectionCallbackPtr)
|
||||
: null;
|
||||
|
||||
// Module callback -- forward to native
|
||||
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
|
||||
// char16 Path[260](520) = 536 bytes
|
||||
NativeModuleCallback nativeModuleCb = (moduleCallbackPtr != IntPtr.Zero)
|
||||
? Marshal.GetDelegateForFunctionPointer<NativeModuleCallback>(moduleCallbackPtr)
|
||||
: null;
|
||||
|
||||
s_functions.EnumerateRemoteSectionsAndModules(handle,
|
||||
// Section callback
|
||||
(ref ReClassNET.Core.EnumerateRemoteSectionData sdata) =>
|
||||
{
|
||||
if (nativeSectionCb == null) return;
|
||||
|
||||
const int kSize = 8 + 8 + 4 + 4 + 4 + 32 + 520; // 580
|
||||
IntPtr mem = Marshal.AllocHGlobal(kSize);
|
||||
try
|
||||
{
|
||||
byte[] z = new byte[kSize];
|
||||
Marshal.Copy(z, 0, mem, kSize);
|
||||
|
||||
Marshal.WriteInt64(mem, 0, sdata.BaseAddress.ToInt64());
|
||||
Marshal.WriteInt64(mem, 8, sdata.Size.ToInt64());
|
||||
Marshal.WriteInt32(mem, 16, (int)sdata.Type);
|
||||
Marshal.WriteInt32(mem, 20, (int)sdata.Category);
|
||||
Marshal.WriteInt32(mem, 24, (int)sdata.Protection);
|
||||
|
||||
if (sdata.Name != null)
|
||||
{
|
||||
char[] c = sdata.Name.ToCharArray();
|
||||
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 28),
|
||||
Math.Min(c.Length, 15));
|
||||
}
|
||||
if (sdata.ModulePath != null)
|
||||
{
|
||||
char[] c = sdata.ModulePath.ToCharArray();
|
||||
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 60),
|
||||
Math.Min(c.Length, 259));
|
||||
}
|
||||
|
||||
nativeSectionCb(mem);
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(mem); }
|
||||
},
|
||||
// Module callback
|
||||
(ref ReClassNET.Core.EnumerateRemoteModuleData mdata) =>
|
||||
{
|
||||
if (nativeModuleCb == null) return;
|
||||
|
||||
const int kSize = 8 + 8 + 520; // 536
|
||||
IntPtr mem = Marshal.AllocHGlobal(kSize);
|
||||
try
|
||||
{
|
||||
byte[] z = new byte[kSize];
|
||||
Marshal.Copy(z, 0, mem, kSize);
|
||||
|
||||
Marshal.WriteInt64(mem, 0, mdata.BaseAddress.ToInt64());
|
||||
Marshal.WriteInt64(mem, 8, mdata.Size.ToInt64());
|
||||
|
||||
if (mdata.Path != null)
|
||||
{
|
||||
char[] c = mdata.Path.ToCharArray();
|
||||
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 16),
|
||||
Math.Min(c.Length, 259));
|
||||
}
|
||||
|
||||
nativeModuleCb(mem);
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(mem); }
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// -- ControlRemoteProcess --
|
||||
private static void ControlProcessImpl(IntPtr handle, int action)
|
||||
{
|
||||
try
|
||||
{
|
||||
s_functions?.ControlRemoteProcess(handle,
|
||||
(ReClassNET.Core.ControlRemoteProcessAction)action);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal file
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<AssemblyName>RcNetBridge</AssemblyName>
|
||||
<RootNamespace>RcNetBridge</RootNamespace>
|
||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
124
plugins/RemoteProcessMemory/CMakeLists.txt
Normal file
124
plugins/RemoteProcessMemory/CMakeLists.txt
Normal file
@@ -0,0 +1,124 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(RemoteProcessMemory 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
|
||||
|
||||
# ─── 1. Payload DLL/SO (no Qt, minimal dependencies) ────────────────
|
||||
|
||||
add_library(rcx_payload SHARED
|
||||
payload/rcx_payload.cpp
|
||||
rcx_rpc_protocol.h
|
||||
)
|
||||
|
||||
set_target_properties(rcx_payload PROPERTIES PREFIX "") # rcx_payload.dll / rcx_payload.so
|
||||
|
||||
target_include_directories(rcx_payload PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(rcx_payload PRIVATE psapi)
|
||||
else()
|
||||
target_link_libraries(rcx_payload PRIVATE pthread rt)
|
||||
target_compile_options(rcx_payload PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
# Output payload to Plugins/ (same dir as plugin DLL, discovered at runtime)
|
||||
set_target_properties(rcx_payload PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
# Install rule: copy both DLLs to install Plugins/ folder
|
||||
install(TARGETS rcx_payload
|
||||
LIBRARY DESTINATION Plugins
|
||||
RUNTIME DESTINATION Plugins
|
||||
)
|
||||
|
||||
# ─── 2. Plugin DLL (Qt, implements IProviderPlugin) ──────────────────
|
||||
|
||||
# Generate ui_processpicker.h in our own build dir (avoids dupbuild with ProcessMemoryPlugin)
|
||||
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 (RemoteProcessMemory)"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(PLUGIN_SOURCES
|
||||
RemoteProcessMemoryPlugin.h
|
||||
RemoteProcessMemoryPlugin.cpp
|
||||
rcx_rpc_protocol.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
"${_UI_HDR}"
|
||||
)
|
||||
|
||||
add_library(RemoteProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE
|
||||
${QT}::Widgets
|
||||
${_QT_WINEXTRAS}
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE psapi shell32)
|
||||
else()
|
||||
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE rt dl)
|
||||
target_compile_options(RemoteProcessMemoryPlugin PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
target_include_directories(RemoteProcessMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h
|
||||
)
|
||||
|
||||
set_target_properties(RemoteProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
install(TARGETS RemoteProcessMemoryPlugin
|
||||
LIBRARY DESTINATION Plugins
|
||||
RUNTIME DESTINATION Plugins
|
||||
)
|
||||
|
||||
# Plugin must be able to find the payload at runtime
|
||||
add_dependencies(RemoteProcessMemoryPlugin rcx_payload)
|
||||
|
||||
# ─── 3. Test executables (no Qt) ────────────────────────────────────
|
||||
|
||||
# Host: loads payload in-process, exposes test buffer
|
||||
add_executable(test_rpc_host tests/test_rpc_host.cpp)
|
||||
target_include_directories(test_rpc_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(WIN32)
|
||||
target_link_libraries(test_rpc_host PRIVATE psapi)
|
||||
else()
|
||||
target_link_libraries(test_rpc_host PRIVATE pthread rt dl)
|
||||
endif()
|
||||
set_target_properties(test_rpc_host PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
add_dependencies(test_rpc_host rcx_payload)
|
||||
|
||||
# Client: connects to host, tests + benchmarks
|
||||
add_executable(test_rpc_client tests/test_rpc_client.cpp)
|
||||
target_include_directories(test_rpc_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(WIN32)
|
||||
target_link_libraries(test_rpc_client PRIVATE psapi)
|
||||
else()
|
||||
target_link_libraries(test_rpc_client PRIVATE pthread rt)
|
||||
endif()
|
||||
set_target_properties(test_rpc_client PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
add_dependencies(test_rpc_client test_rpc_host)
|
||||
927
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp
Normal file
927
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp
Normal file
@@ -0,0 +1,927 @@
|
||||
#include "RemoteProcessMemoryPlugin.h"
|
||||
#include "rcx_rpc_protocol.h"
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
#include <QStyle>
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
#include <QImage>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
|
||||
#include <QtWin>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
# include <tlhelp32.h>
|
||||
# include <psapi.h>
|
||||
# include <shellapi.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
# include <dlfcn.h>
|
||||
# include <sys/mman.h>
|
||||
# include <sys/wait.h>
|
||||
# include <sys/ptrace.h>
|
||||
# include <sys/user.h>
|
||||
# include <semaphore.h>
|
||||
# include <signal.h>
|
||||
# include <link.h>
|
||||
# include <climits>
|
||||
# include <cstring>
|
||||
# include <fstream>
|
||||
# include <sstream>
|
||||
#endif
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* IPC Client
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
struct IpcClient {
|
||||
#ifdef _WIN32
|
||||
HANDLE hShm = nullptr;
|
||||
HANDLE hReqEvent = nullptr;
|
||||
HANDLE hRspEvent = nullptr;
|
||||
#else
|
||||
int shmFd = -1;
|
||||
sem_t* reqSem = SEM_FAILED;
|
||||
sem_t* rspSem = SEM_FAILED;
|
||||
char shmNameBuf[128] = {};
|
||||
char reqNameBuf[128] = {};
|
||||
char rspNameBuf[128] = {};
|
||||
#endif
|
||||
void* mappedView = nullptr;
|
||||
QMutex mutex;
|
||||
bool connected = false;
|
||||
|
||||
~IpcClient() { disconnect(); }
|
||||
|
||||
/* ── connect / disconnect ──────────────────────────────────────── */
|
||||
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
/* poll for shared memory to appear (payload creating it) */
|
||||
auto deadline = GetTickCount64() + (uint64_t)timeoutMs;
|
||||
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(10);
|
||||
}
|
||||
|
||||
mappedView = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!mappedView) { CloseHandle(hShm); hShm = nullptr; return false; }
|
||||
|
||||
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
|
||||
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
|
||||
if (!hReqEvent || !hRspEvent) { disconnect(); return false; }
|
||||
#else
|
||||
strncpy(shmNameBuf, shmName, sizeof(shmNameBuf) - 1);
|
||||
strncpy(reqNameBuf, reqName, sizeof(reqNameBuf) - 1);
|
||||
strncpy(rspNameBuf, rspName, sizeof(rspNameBuf) - 1);
|
||||
|
||||
/* poll for shared memory */
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
shmFd = shm_open(shmName, O_RDWR, 0);
|
||||
if (shmFd >= 0) break;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(10000);
|
||||
}
|
||||
|
||||
mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shmFd, 0);
|
||||
if (mappedView == MAP_FAILED) { mappedView = nullptr; close(shmFd); shmFd = -1; return false; }
|
||||
|
||||
reqSem = sem_open(reqName, 0);
|
||||
rspSem = sem_open(rspName, 0);
|
||||
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) { disconnect(); return false; }
|
||||
#endif
|
||||
|
||||
/* wait for payloadReady */
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
#ifdef _WIN32
|
||||
while (!hdr->payloadReady) {
|
||||
if (GetTickCount64() >= deadline) { disconnect(); return false; }
|
||||
Sleep(5);
|
||||
}
|
||||
#else
|
||||
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) { disconnect(); return false; }
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
|
||||
connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (mappedView) { UnmapViewOfFile(mappedView); mappedView = nullptr; }
|
||||
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
|
||||
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
|
||||
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
|
||||
#else
|
||||
if (mappedView) { munmap(mappedView, RCX_RPC_SHM_SIZE); mappedView = nullptr; }
|
||||
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
|
||||
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
|
||||
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
|
||||
#endif
|
||||
connected = false;
|
||||
}
|
||||
|
||||
/* ── low-level RPC round-trip ──────────────────────────────────── */
|
||||
|
||||
bool signalAndWait(int timeoutMs = 2000)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
SetEvent(hReqEvent);
|
||||
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
|
||||
#else
|
||||
sem_post(reqSem);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += timeoutMs / 1000;
|
||||
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
return sem_timedwait(rspSem, &ts) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── public API ────────────────────────────────────────────────── */
|
||||
|
||||
bool readSingle(uint64_t addr, void* buf, int len)
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected || len <= 0) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = 1;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
entry->address = addr;
|
||||
entry->length = (uint32_t)len;
|
||||
entry->dataOffset = sizeof(RcxRpcReadEntry);
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
|
||||
memcpy(buf, data + entry->dataOffset, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeSingle(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected || len <= 0) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_WRITE;
|
||||
hdr->writeAddress = addr;
|
||||
hdr->writeLength = (uint32_t)len;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
memcpy(data, buf, len);
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
|
||||
return hdr->status == RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
QVector<RemoteProcessProvider::ModuleInfo> enumerateModules()
|
||||
{
|
||||
QVector<RemoteProcessProvider::ModuleInfo> result;
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return result;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_ENUM_MODULES;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) { connected = false; return result; }
|
||||
if (hdr->status != RCX_RPC_STATUS_OK) return result;
|
||||
|
||||
uint32_t count = hdr->responseCount;
|
||||
result.reserve((int)count);
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* entry = reinterpret_cast<const RcxRpcModuleEntry*>(
|
||||
data + i * sizeof(RcxRpcModuleEntry));
|
||||
|
||||
QString modName;
|
||||
#ifdef _WIN32
|
||||
modName = QString::fromWCharArray(
|
||||
reinterpret_cast<const wchar_t*>(data + entry->nameOffset),
|
||||
(int)(entry->nameLength / sizeof(wchar_t)));
|
||||
#else
|
||||
modName = QString::fromUtf8(
|
||||
reinterpret_cast<const char*>(data + entry->nameOffset),
|
||||
(int)entry->nameLength);
|
||||
#endif
|
||||
result.append({modName, entry->base, entry->size});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ping()
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
hdr->command = RPC_CMD_PING;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown()
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
hdr->command = RPC_CMD_SHUTDOWN;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
signalAndWait(500);
|
||||
connected = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* RemoteProcessProvider
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
RemoteProcessProvider::RemoteProcessProvider(
|
||||
uint32_t pid, const QString& processName,
|
||||
std::shared_ptr<IpcClient> ipc)
|
||||
: m_pid(pid)
|
||||
, m_processName(processName)
|
||||
, m_connected(ipc && ipc->connected)
|
||||
, m_base(0)
|
||||
, m_ipc(std::move(ipc))
|
||||
{
|
||||
if (m_connected)
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
RemoteProcessProvider::~RemoteProcessProvider() = default;
|
||||
|
||||
bool RemoteProcessProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_connected || len <= 0) return false;
|
||||
bool ok = m_ipc->readSingle(addr, buf, len);
|
||||
if (!ok) {
|
||||
memset(buf, 0, (size_t)len);
|
||||
/* update connectivity flag through mutable ipc */
|
||||
const_cast<RemoteProcessProvider*>(this)->m_connected = m_ipc->connected;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
int RemoteProcessProvider::size() const
|
||||
{
|
||||
return m_connected ? 0x10000 : 0;
|
||||
}
|
||||
|
||||
bool RemoteProcessProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_connected || len <= 0) return false;
|
||||
bool ok = m_ipc->writeSingle(addr, buf, len);
|
||||
if (!ok) m_connected = m_ipc->connected;
|
||||
return ok;
|
||||
}
|
||||
|
||||
QString RemoteProcessProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (addr >= mod.base && addr < mod.base + mod.size) {
|
||||
uint64_t off = addr - mod.base;
|
||||
return QStringLiteral("%1+0x%2")
|
||||
.arg(mod.name)
|
||||
.arg(off, 0, 16, QChar('0'));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
uint64_t RemoteProcessProvider::symbolToAddress(const QString& n) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.name.compare(n, Qt::CaseInsensitive) == 0)
|
||||
return mod.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RemoteProcessProvider::cacheModules()
|
||||
{
|
||||
m_modules = m_ipc->enumerateModules();
|
||||
if (!m_modules.isEmpty())
|
||||
m_base = m_modules.first().base;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Injection helpers
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
namespace {
|
||||
|
||||
/* Resolve payload DLL/SO path next to this plugin DLL/SO */
|
||||
static QString payloadPath()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
HMODULE hSelf = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&payloadPath), &hSelf);
|
||||
WCHAR buf[MAX_PATH];
|
||||
GetModuleFileNameW(hSelf, buf, MAX_PATH);
|
||||
QFileInfo fi(QString::fromWCharArray(buf));
|
||||
return fi.absolutePath() + QStringLiteral("/rcx_payload.dll");
|
||||
#else
|
||||
Dl_info info;
|
||||
dladdr(reinterpret_cast<void*>(&payloadPath), &info);
|
||||
QFileInfo fi(QString::fromUtf8(info.dli_fname));
|
||||
return fi.absolutePath() + QStringLiteral("/rcx_payload.so");
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
|
||||
|
||||
static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QString path = payloadPath();
|
||||
QByteArray pathUtf8 = QDir::toNativeSeparators(path).toLocal8Bit();
|
||||
|
||||
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
|
||||
if (!hProc) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("OpenProcess failed (error %1).\n"
|
||||
"Try running as Administrator.")
|
||||
.arg(GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
/* allocate + write path string in target */
|
||||
SIZE_T pathLen = (SIZE_T)(pathUtf8.size() + 1);
|
||||
void* remotePath = VirtualAllocEx(hProc, nullptr, pathLen,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!remotePath) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("VirtualAllocEx failed.");
|
||||
CloseHandle(hProc);
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
|
||||
|
||||
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
|
||||
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
|
||||
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
||||
GetProcAddress(hK32, "LoadLibraryA"));
|
||||
|
||||
HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0,
|
||||
pLoadLib, remotePath, 0, nullptr);
|
||||
if (!hThread) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("CreateRemoteThread failed (error %1).")
|
||||
.arg(GetLastError());
|
||||
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
||||
CloseHandle(hProc);
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(hThread, 10000);
|
||||
|
||||
DWORD exitCode = 0;
|
||||
GetExitCodeThread(hThread, &exitCode);
|
||||
CloseHandle(hThread);
|
||||
|
||||
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
||||
|
||||
if (exitCode == 0) {
|
||||
CloseHandle(hProc);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
|
||||
"Ensure rcx_payload.dll is in: %1").arg(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
|
||||
(loader lock is no longer held after LoadLibrary returned) */
|
||||
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
|
||||
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
|
||||
GetProcAddress(hK32, "GetProcAddress"));
|
||||
|
||||
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
|
||||
const char initName[] = "RcxPayloadInit";
|
||||
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (remoteInitName) {
|
||||
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
|
||||
|
||||
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
|
||||
Simpler approach: write small shellcode that does both calls. */
|
||||
uint8_t shellcode[128];
|
||||
int off = 0;
|
||||
|
||||
/* sub rsp, 40 ; shadow space + alignment */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
|
||||
/* mov rcx, hPayloadRemote ; first arg = module handle */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
|
||||
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
|
||||
memcpy(shellcode + off, &hMod, 8); off += 8;
|
||||
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
|
||||
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
|
||||
memcpy(shellcode + off, &pName, 8); off += 8;
|
||||
/* mov rax, GetProcAddress */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
|
||||
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
|
||||
memcpy(shellcode + off, &pGPA, 8); off += 8;
|
||||
/* call rax ; rax = RcxPayloadInit */
|
||||
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
||||
/* test rax, rax */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
|
||||
/* jz skip (jump over the call if null) */
|
||||
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
|
||||
/* call rax ; RcxPayloadInit() */
|
||||
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
||||
/* skip: add rsp, 40 */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
|
||||
/* ret */
|
||||
shellcode[off++] = 0xC3;
|
||||
|
||||
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
if (remoteCode) {
|
||||
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
|
||||
|
||||
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
|
||||
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
|
||||
if (hThread2) {
|
||||
WaitForSingleObject(hThread2, 10000);
|
||||
CloseHandle(hThread2);
|
||||
}
|
||||
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
|
||||
}
|
||||
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
|
||||
}
|
||||
|
||||
CloseHandle(hProc);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
/* ── Linux injection: ptrace + dlopen ─────────────────────────────── */
|
||||
|
||||
static uint64_t findLibBase(pid_t pid, const char* libName)
|
||||
{
|
||||
char mapsPath[64];
|
||||
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
|
||||
FILE* f = fopen(mapsPath, "r");
|
||||
if (!f) return 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strstr(line, libName)) {
|
||||
uint64_t base;
|
||||
if (sscanf(line, "%lx-", &base) == 1) {
|
||||
fclose(f);
|
||||
return base;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t findSyscallInsn(pid_t pid)
|
||||
{
|
||||
char mapsPath[64];
|
||||
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
|
||||
FILE* f = fopen(mapsPath, "r");
|
||||
if (!f) return 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strstr(line, "libc") && strstr(line, "r-xp")) {
|
||||
uint64_t start, end;
|
||||
if (sscanf(line, "%lx-%lx", &start, &end) != 2) continue;
|
||||
fclose(f);
|
||||
|
||||
/* scan for 0F 05 (syscall) */
|
||||
char memPath[64];
|
||||
snprintf(memPath, sizeof(memPath), "/proc/%d/mem", pid);
|
||||
int memFd = open(memPath, O_RDONLY);
|
||||
if (memFd < 0) return 0;
|
||||
|
||||
uint8_t buf[4096];
|
||||
for (uint64_t off = start; off < end; off += sizeof(buf)) {
|
||||
ssize_t n = pread(memFd, buf, sizeof(buf), (off_t)off);
|
||||
if (n <= 1) break;
|
||||
for (ssize_t i = 0; i + 1 < n; ++i) {
|
||||
if (buf[i] == 0x0F && buf[i + 1] == 0x05) {
|
||||
close(memFd);
|
||||
return off + (uint64_t)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
close(memFd);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool writeTargetMem(pid_t pid, uint64_t addr, const void* src, size_t len)
|
||||
{
|
||||
const uint8_t* p = static_cast<const uint8_t*>(src);
|
||||
for (size_t i = 0; i < len; i += sizeof(long)) {
|
||||
long val = 0;
|
||||
size_t chunk = (len - i < sizeof(long)) ? (len - i) : sizeof(long);
|
||||
if (chunk < sizeof(long)) {
|
||||
errno = 0;
|
||||
val = ptrace(PTRACE_PEEKDATA, pid, (void*)(addr + i), nullptr);
|
||||
if (errno) return false;
|
||||
}
|
||||
memcpy(&val, p + i, chunk);
|
||||
if (ptrace(PTRACE_POKEDATA, pid, (void*)(addr + i), (void*)val) < 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QString path = payloadPath();
|
||||
QByteArray pathUtf8 = path.toUtf8();
|
||||
|
||||
if (ptrace(PTRACE_ATTACH, (pid_t)pid, nullptr, nullptr) < 0) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("ptrace attach failed: %1\n"
|
||||
"Check /proc/sys/kernel/yama/ptrace_scope or run as root.")
|
||||
.arg(strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int status;
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
/* save registers */
|
||||
struct user_regs_struct savedRegs, regs;
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
regs = savedRegs;
|
||||
|
||||
/* find syscall instruction in target's libc */
|
||||
uint64_t syscallAddr = findSyscallInsn((pid_t)pid);
|
||||
if (!syscallAddr) {
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Could not find syscall instruction in target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* find dlopen in target via libc offset technique */
|
||||
void* ourDlopen = dlsym(RTLD_DEFAULT, "dlopen");
|
||||
uint64_t ourLibcBase = findLibBase(getpid(), "libc");
|
||||
uint64_t targetLibcBase = findLibBase((pid_t)pid, "libc");
|
||||
|
||||
if (!ourDlopen || !ourLibcBase || !targetLibcBase) {
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Could not resolve dlopen address.");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t targetDlopen = targetLibcBase + ((uint64_t)ourDlopen - ourLibcBase);
|
||||
|
||||
/* call mmap in target via syscall: mmap(0, 4096, RWX, MAP_PRIVATE|MAP_ANON, -1, 0) */
|
||||
regs.rax = 9; /* __NR_mmap */
|
||||
regs.rdi = 0;
|
||||
regs.rsi = 4096;
|
||||
regs.rdx = 7; /* PROT_READ|PROT_WRITE|PROT_EXEC */
|
||||
regs.r10 = 0x22; /* MAP_PRIVATE|MAP_ANONYMOUS */
|
||||
regs.r8 = (uint64_t)-1;
|
||||
regs.r9 = 0;
|
||||
regs.rip = syscallAddr;
|
||||
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s);
|
||||
uint64_t mmapPage = regs.rax;
|
||||
|
||||
if ((int64_t)mmapPage < 0 || mmapPage == 0) {
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("mmap in target failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* write path string at start of page */
|
||||
writeTargetMem((pid_t)pid, mmapPage, pathUtf8.constData(), (size_t)(pathUtf8.size() + 1));
|
||||
|
||||
/* write shellcode after path:
|
||||
* mov rdi, pathAddr (48 BF xxxxxxxx)
|
||||
* mov rsi, 2 (48 BE 02000000 00000000)
|
||||
* mov rax, dlopenAddr (48 B8 xxxxxxxx)
|
||||
* call rax (FF D0)
|
||||
* int3 (CC)
|
||||
*/
|
||||
uint64_t pathAddr = mmapPage;
|
||||
uint64_t codeAddr = mmapPage + ((pathUtf8.size() + 1 + 15) & ~15ULL);
|
||||
|
||||
uint8_t sc[64];
|
||||
int len = 0;
|
||||
/* mov rdi, imm64 */
|
||||
sc[len++] = 0x48; sc[len++] = 0xBF;
|
||||
memcpy(sc + len, &pathAddr, 8); len += 8;
|
||||
/* mov rsi, 2 (RTLD_NOW) */
|
||||
sc[len++] = 0x48; sc[len++] = 0xBE;
|
||||
uint64_t rtldNow = 2;
|
||||
memcpy(sc + len, &rtldNow, 8); len += 8;
|
||||
/* mov rax, dlopen */
|
||||
sc[len++] = 0x48; sc[len++] = 0xB8;
|
||||
memcpy(sc + len, &targetDlopen, 8); len += 8;
|
||||
/* call rax */
|
||||
sc[len++] = 0xFF; sc[len++] = 0xD0;
|
||||
/* int3 */
|
||||
sc[len++] = 0xCC;
|
||||
|
||||
writeTargetMem((pid_t)pid, codeAddr, sc, (size_t)len);
|
||||
|
||||
/* execute shellcode */
|
||||
regs = savedRegs;
|
||||
regs.rip = codeAddr;
|
||||
regs.rsp = (mmapPage + 4096) & ~0xFULL;
|
||||
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ptrace(PTRACE_CONT, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
bool ok = false;
|
||||
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ok = (regs.rax != 0);
|
||||
}
|
||||
|
||||
/* clean up: munmap the page via syscall */
|
||||
struct user_regs_struct cleanRegs = savedRegs;
|
||||
cleanRegs.rax = 11; /* __NR_munmap */
|
||||
cleanRegs.rdi = mmapPage;
|
||||
cleanRegs.rsi = 4096;
|
||||
cleanRegs.rip = syscallAddr;
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &cleanRegs);
|
||||
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
/* restore and detach */
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
|
||||
if (!ok && errorMsg)
|
||||
*errorMsg = QStringLiteral("dlopen failed in target.\n"
|
||||
"Ensure payload is at: %1").arg(path);
|
||||
return ok;
|
||||
}
|
||||
#endif /* _WIN32 / linux injection */
|
||||
|
||||
} /* anonymous namespace */
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* RemoteProcessMemoryPlugin
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
RemoteProcessMemoryPlugin::RemoteProcessMemoryPlugin() = default;
|
||||
RemoteProcessMemoryPlugin::~RemoteProcessMemoryPlugin() = default;
|
||||
|
||||
QIcon RemoteProcessMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon);
|
||||
}
|
||||
|
||||
bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
return target.startsWith(QStringLiteral("rpm:"));
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider>
|
||||
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
/* target = "rpm:{pid}:{name}" */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
uint32_t pid = parts[1].toUInt(&ok);
|
||||
QString name = parts.mid(2).join(':'); /* name may contain colons */
|
||||
|
||||
if (!ok || pid == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto ipc = getOrCreateConnection(pid, errorMsg);
|
||||
if (!ipc) return nullptr;
|
||||
|
||||
return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
|
||||
}
|
||||
|
||||
uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
/* Read imageBase directly from the shared-memory header -- zero IPC cost.
|
||||
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
|
||||
return 0;
|
||||
|
||||
bool ok;
|
||||
uint32_t pid = parts[1].toUInt(&ok);
|
||||
if (!ok) return 0;
|
||||
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
auto it = m_connections.constFind(pid);
|
||||
if (it == m_connections.constEnd() || !(*it)->connected)
|
||||
return 0;
|
||||
|
||||
auto* hdr = static_cast<const RcxRpcHeader*>((*it)->mappedView);
|
||||
return hdr->imageBase;
|
||||
}
|
||||
|
||||
bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
/* ── 1. pick a process ── */
|
||||
QVector<PluginProcessInfo> pluginProcs = enumerateProcesses();
|
||||
QList<ProcessInfo> procs;
|
||||
for (const auto& pi : pluginProcs) {
|
||||
ProcessInfo info;
|
||||
info.pid = pi.pid;
|
||||
info.name = pi.name;
|
||||
info.path = pi.path;
|
||||
info.icon = pi.icon;
|
||||
procs.append(info);
|
||||
}
|
||||
|
||||
ProcessPicker picker(procs, parent);
|
||||
if (picker.exec() != QDialog::Accepted) return false;
|
||||
|
||||
uint32_t pid = picker.selectedProcessId();
|
||||
QString name = picker.selectedProcessName();
|
||||
|
||||
/* ── 2. ask inject or connect ── */
|
||||
QMessageBox box(parent);
|
||||
box.setWindowTitle(QStringLiteral("Remote Process Memory"));
|
||||
box.setText(QStringLiteral("Connect to %1 (PID %2)").arg(name).arg(pid));
|
||||
box.setInformativeText(QStringLiteral("Choose how to connect to the target:"));
|
||||
QAbstractButton* injectBtn = box.addButton(QStringLiteral("Inject Payload"), QMessageBox::ActionRole);
|
||||
QAbstractButton* connectBtn = box.addButton(QStringLiteral("Already Injected"), QMessageBox::ActionRole);
|
||||
box.addButton(QMessageBox::Cancel);
|
||||
box.exec();
|
||||
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
if (clicked == injectBtn) {
|
||||
QString injectErr;
|
||||
if (!injectPayload(pid, &injectErr)) {
|
||||
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
|
||||
return false;
|
||||
}
|
||||
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
else if (clicked == connectBtn) {
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> procs;
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return procs;
|
||||
|
||||
PROCESSENTRY32W entry;
|
||||
entry.dwSize = sizeof(entry);
|
||||
|
||||
if (Process32FirstW(snap, &entry)) {
|
||||
do {
|
||||
PluginProcessInfo info;
|
||||
info.pid = entry.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(entry.szExeFile);
|
||||
|
||||
HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE, entry.th32ProcessID);
|
||||
if (hProc) {
|
||||
wchar_t path[MAX_PATH * 2];
|
||||
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
|
||||
if (QueryFullProcessImageNameW(hProc, 0, path, &pathLen)) {
|
||||
info.path = QString::fromWCharArray(path);
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi),
|
||||
SHGFI_ICON | SHGFI_SMALLICON) && sfi.hIcon) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
info.icon = QIcon(QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)));
|
||||
#else
|
||||
info.icon = QIcon(QtWin::fromHICON(sfi.hIcon));
|
||||
#endif
|
||||
DestroyIcon(sfi.hIcon);
|
||||
}
|
||||
}
|
||||
CloseHandle(hProc);
|
||||
}
|
||||
procs.append(info);
|
||||
} while (Process32NextW(snap, &entry));
|
||||
}
|
||||
CloseHandle(snap);
|
||||
|
||||
#else
|
||||
QDir procDir(QStringLiteral("/proc"));
|
||||
QIcon defIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
|
||||
for (const QString& entry : procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
bool ok;
|
||||
uint32_t pid = entry.toUInt(&ok);
|
||||
if (!ok || pid == 0) continue;
|
||||
|
||||
QFile commFile(QStringLiteral("/proc/%1/comm").arg(pid));
|
||||
if (!commFile.open(QIODevice::ReadOnly)) continue;
|
||||
QString procName = QString::fromUtf8(commFile.readAll()).trimmed();
|
||||
commFile.close();
|
||||
if (procName.isEmpty()) continue;
|
||||
|
||||
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
|
||||
if (::access(memPath.toUtf8().constData(), R_OK) != 0) continue;
|
||||
|
||||
QFileInfo exeInfo(QStringLiteral("/proc/%1/exe").arg(pid));
|
||||
PluginProcessInfo info;
|
||||
info.pid = pid;
|
||||
info.name = procName;
|
||||
info.path = exeInfo.exists() ? exeInfo.symLinkTarget() : QString();
|
||||
info.icon = defIcon;
|
||||
procs.append(info);
|
||||
}
|
||||
#endif
|
||||
|
||||
return procs;
|
||||
}
|
||||
|
||||
std::shared_ptr<IpcClient>
|
||||
RemoteProcessMemoryPlugin::getOrCreateConnection(
|
||||
uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
|
||||
auto it = m_connections.find(pid);
|
||||
if (it != m_connections.end() && (*it)->connected)
|
||||
return *it;
|
||||
|
||||
auto ipc = std::make_shared<IpcClient>();
|
||||
if (!ipc->connect(pid)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
|
||||
"Is the payload running?").arg(pid);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_connections[pid] = ipc;
|
||||
return ipc;
|
||||
}
|
||||
|
||||
/* ── Plugin factory ───────────────────────────────────────────────── */
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new RemoteProcessMemoryPlugin();
|
||||
}
|
||||
86
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h
Normal file
86
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "../../src/providers/provider.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <QMutex>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
|
||||
struct IpcClient; /* defined in .cpp */
|
||||
|
||||
/* ── Provider ─────────────────────────────────────────────────────── */
|
||||
|
||||
class RemoteProcessProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
struct ModuleInfo { QString name; uint64_t base; uint64_t size; };
|
||||
|
||||
RemoteProcessProvider(uint32_t pid, const QString& processName,
|
||||
std::shared_ptr<IpcClient> ipc);
|
||||
~RemoteProcessProvider() override;
|
||||
|
||||
/* required */
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
/* optional */
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_connected; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("RemoteProcess"); }
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
uint64_t symbolToAddress(const QString& n) const override;
|
||||
|
||||
uint32_t pid() const { return m_pid; }
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
bool m_connected;
|
||||
uint64_t m_base;
|
||||
mutable std::shared_ptr<IpcClient> m_ipc;
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
|
||||
/* ── Plugin ───────────────────────────────────────────────────────── */
|
||||
|
||||
class RemoteProcessMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
RemoteProcessMemoryPlugin();
|
||||
~RemoteProcessMemoryPlugin() override;
|
||||
|
||||
std::string Name() const override { return "Remote Process Memory"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override {
|
||||
return "Read/write memory via injected payload (shared-memory IPC)";
|
||||
}
|
||||
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;
|
||||
|
||||
private:
|
||||
std::shared_ptr<IpcClient> getOrCreateConnection(
|
||||
uint32_t pid, QString* errorMsg);
|
||||
|
||||
mutable QMutex m_connectionsMutex;
|
||||
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;
|
||||
};
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
612
plugins/RemoteProcessMemory/payload/rcx_payload.cpp
Normal file
612
plugins/RemoteProcessMemory/payload/rcx_payload.cpp
Normal file
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* rcx_payload -- injected into target process.
|
||||
*
|
||||
* Pure Win32 / POSIX, NO Qt, minimal footprint.
|
||||
* Creates the main IPC channel (shared memory + events/semaphores)
|
||||
* using PID-only naming and uses a timer queue for polling.
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ===================================================================
|
||||
* WINDOWS implementation
|
||||
* =================================================================== */
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
|
||||
/* ── globals ──────────────────────────────────────────────────────── */
|
||||
static HANDLE g_hShm = nullptr;
|
||||
static void* g_mappedView = nullptr;
|
||||
static HANDLE g_hReqEvent = nullptr;
|
||||
static HANDLE g_hRspEvent = nullptr;
|
||||
static HANDLE g_hTimerQueue = nullptr;
|
||||
static HANDLE g_hPollTimer = nullptr;
|
||||
static volatile LONG g_initialized = 0;
|
||||
|
||||
/* ── memory safety via VirtualQuery ────────────────────────────────── */
|
||||
|
||||
inline bool IsReadableProtect(DWORD p)
|
||||
{
|
||||
if (p & (PAGE_NOACCESS | PAGE_GUARD))
|
||||
return false;
|
||||
|
||||
const DWORD readable =
|
||||
PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
|
||||
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
|
||||
return (p & readable) != 0;
|
||||
}
|
||||
|
||||
inline bool IsWritableProtect(DWORD p)
|
||||
{
|
||||
if (p & (PAGE_NOACCESS | PAGE_GUARD))
|
||||
return false;
|
||||
|
||||
const DWORD writable =
|
||||
PAGE_READWRITE | PAGE_WRITECOPY |
|
||||
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
|
||||
return (p & writable) != 0;
|
||||
}
|
||||
|
||||
/* Check that the full range [addr, addr+len) is covered by readable pages. */
|
||||
static bool IsRangeReadable(uintptr_t addr, uint32_t len)
|
||||
{
|
||||
uintptr_t end = addr + len;
|
||||
uintptr_t cur = addr;
|
||||
while (cur < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
|
||||
return false;
|
||||
if (mbi.State != MEM_COMMIT || !IsReadableProtect(mbi.Protect))
|
||||
return false;
|
||||
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
cur = regionEnd;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsRangeWritable(uintptr_t addr, uint32_t len)
|
||||
{
|
||||
uintptr_t end = addr + len;
|
||||
uintptr_t cur = addr;
|
||||
while (cur < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
|
||||
return false;
|
||||
if (mbi.State != MEM_COMMIT || !IsWritableProtect(mbi.Protect))
|
||||
return false;
|
||||
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
cur = regionEnd;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── command handlers ─────────────────────────────────────────────── */
|
||||
|
||||
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
|
||||
uint8_t* dest = data + entries[i].dataOffset;
|
||||
uintptr_t src = static_cast<uintptr_t>(entries[i].address);
|
||||
if (IsRangeReadable(src, entries[i].length)) {
|
||||
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
|
||||
} else {
|
||||
memset(dest, 0, entries[i].length);
|
||||
hdr->status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
/* SEH fallback (commented out, kept for reference):
|
||||
__try {
|
||||
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
memset(dest, 0, entries[i].length);
|
||||
hdr->status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
*/
|
||||
}
|
||||
hdr->responseCount = hdr->requestCount;
|
||||
}
|
||||
|
||||
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
uintptr_t dst = static_cast<uintptr_t>(hdr->writeAddress);
|
||||
if (IsRangeWritable(dst, hdr->writeLength)) {
|
||||
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
|
||||
} else {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
/* SEH fallback (commented out, kept for reference):
|
||||
__try {
|
||||
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
HANDLE hProc = GetCurrentProcess();
|
||||
HMODULE mods[1024];
|
||||
DWORD needed = 0;
|
||||
if (!EnumProcessModules(hProc, mods, sizeof(mods), &needed)) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
hdr->responseCount = 0;
|
||||
return;
|
||||
}
|
||||
int count = (int)(needed / sizeof(HMODULE));
|
||||
if (count > 1024) count = 1024;
|
||||
|
||||
uint32_t entryBytes = (uint32_t)(count * sizeof(RcxRpcModuleEntry));
|
||||
uint32_t nameDataOff = entryBytes;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
MODULEINFO mi{};
|
||||
WCHAR modName[MAX_PATH];
|
||||
GetModuleInformation(hProc, mods[i], &mi, sizeof(mi));
|
||||
int nameLen = (int)GetModuleBaseNameW(hProc, mods[i], modName, MAX_PATH);
|
||||
uint32_t nameBytes = (uint32_t)(nameLen * sizeof(WCHAR));
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(data + i * sizeof(RcxRpcModuleEntry));
|
||||
entry->base = reinterpret_cast<uint64_t>(mi.lpBaseOfDll);
|
||||
entry->size = static_cast<uint64_t>(mi.SizeOfImage);
|
||||
entry->nameOffset = nameDataOff;
|
||||
entry->nameLength = nameBytes;
|
||||
|
||||
if (nameDataOff + nameBytes <= RCX_RPC_DATA_SIZE) {
|
||||
memcpy(data + nameDataOff, modName, nameBytes);
|
||||
nameDataOff += nameBytes;
|
||||
}
|
||||
}
|
||||
|
||||
hdr->responseCount = (uint32_t)count;
|
||||
hdr->totalDataUsed = nameDataOff;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
/* forward declaration */
|
||||
void RcxPayloadCleanup();
|
||||
|
||||
/* ── timer callback (non-blocking poll) ───────────────────────────── */
|
||||
|
||||
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
|
||||
{
|
||||
if (!g_mappedView || !g_hReqEvent || !g_hRspEvent)
|
||||
return;
|
||||
|
||||
/* non-blocking check: is there a pending request? */
|
||||
DWORD rc = WaitForSingleObject(g_hReqEvent, 0);
|
||||
if (rc != WAIT_OBJECT_0)
|
||||
return;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
switch (static_cast<RcxRpcCommand>(hdr->command)) {
|
||||
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
|
||||
case RPC_CMD_WRITE: handle_write(hdr, data); break;
|
||||
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
|
||||
case RPC_CMD_PING: break;
|
||||
case RPC_CMD_SHUTDOWN:
|
||||
RcxPayloadCleanup();
|
||||
return;
|
||||
default:
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
SetEvent(g_hRspEvent);
|
||||
}
|
||||
|
||||
/* ── cleanup ──────────────────────────────────────────────────────── */
|
||||
|
||||
void RcxPayloadCleanup()
|
||||
{
|
||||
if (!InterlockedCompareExchange(&g_initialized, 0, 0))
|
||||
return;
|
||||
|
||||
/* stop the poll timer first */
|
||||
if (g_hTimerQueue) {
|
||||
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
|
||||
g_hTimerQueue = nullptr;
|
||||
g_hPollTimer = nullptr;
|
||||
}
|
||||
|
||||
/* mark not-ready */
|
||||
if (g_mappedView) {
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
|
||||
}
|
||||
|
||||
if (g_mappedView) { UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
|
||||
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
|
||||
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
|
||||
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
|
||||
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
}
|
||||
|
||||
/* ── init (called AFTER DllMain returns — safe for timer queues) ── */
|
||||
|
||||
extern "C" __declspec(dllexport)
|
||||
bool RcxPayloadInit()
|
||||
{
|
||||
if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
|
||||
return true; /* already initialized */
|
||||
|
||||
uint32_t pid = GetCurrentProcessId();
|
||||
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
|
||||
if (!g_hShm) {
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!g_mappedView) {
|
||||
CloseHandle(g_hShm); g_hShm = nullptr;
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
hdr->version = RCX_RPC_VERSION;
|
||||
|
||||
/* image base from PEB */
|
||||
{
|
||||
uint64_t peb;
|
||||
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
|
||||
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
|
||||
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10);
|
||||
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30);
|
||||
}
|
||||
|
||||
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
|
||||
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
|
||||
if (!g_hReqEvent || !g_hRspEvent) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* create dedicated timer queue + fast poll timer (10ms interval) */
|
||||
g_hTimerQueue = CreateTimerQueue();
|
||||
if (!g_hTimerQueue) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CreateTimerQueueTimer(&g_hPollTimer, g_hTimerQueue,
|
||||
RcxPollTimerCallback, nullptr,
|
||||
0, /* start immediately */
|
||||
10, /* 10ms repeat */
|
||||
WT_EXECUTEDEFAULT)) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* mark ready */
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── DllMain — minimal, no heavy work under loader lock ───────────── */
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
|
||||
{
|
||||
if (reason == DLL_PROCESS_DETACH) {
|
||||
RcxPayloadCleanup();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#else
|
||||
/* ===================================================================
|
||||
* LINUX implementation
|
||||
* =================================================================== */
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <semaphore.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
|
||||
/* ── globals ──────────────────────────────────────────────────────── */
|
||||
static int g_shmFd = -1;
|
||||
static void* g_mappedView = nullptr;
|
||||
static sem_t* g_reqSem = SEM_FAILED;
|
||||
static sem_t* g_rspSem = SEM_FAILED;
|
||||
static pthread_t g_thread;
|
||||
static volatile int g_shutdown = 0;
|
||||
static volatile int g_threadRunning = 0;
|
||||
static int g_memFd = -1; /* /proc/self/mem for safe access */
|
||||
static char g_shmName[128];
|
||||
static char g_reqName[128];
|
||||
static char g_rspName[128];
|
||||
|
||||
/* ── safe memory access via /proc/self/mem ────────────────────────── */
|
||||
|
||||
static void safe_read(uint64_t addr, void* dest, uint32_t len, uint32_t* status)
|
||||
{
|
||||
ssize_t n = pread(g_memFd, dest, len, (off_t)addr);
|
||||
if (n < (ssize_t)len) {
|
||||
if (n > 0)
|
||||
memset((uint8_t*)dest + n, 0, len - (uint32_t)n);
|
||||
else
|
||||
memset(dest, 0, len);
|
||||
*status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
}
|
||||
|
||||
static void safe_write(uint64_t addr, const void* src, uint32_t len, uint32_t* status)
|
||||
{
|
||||
ssize_t n = pwrite(g_memFd, src, len, (off_t)addr);
|
||||
if (n < (ssize_t)len)
|
||||
*status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
|
||||
/* ── command handlers ─────────────────────────────────────────────── */
|
||||
|
||||
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
|
||||
uint8_t* dest = data + entries[i].dataOffset;
|
||||
safe_read(entries[i].address, dest, entries[i].length, &hdr->status);
|
||||
}
|
||||
hdr->responseCount = hdr->requestCount;
|
||||
}
|
||||
|
||||
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
safe_write(hdr->writeAddress, data, hdr->writeLength, &hdr->status);
|
||||
}
|
||||
|
||||
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
FILE* f = fopen("/proc/self/maps", "r");
|
||||
if (!f) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
hdr->responseCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
/* first pass: collect unique file-backed mappings */
|
||||
struct ModRange { uint64_t base; uint64_t end; char path[512]; };
|
||||
static ModRange modules[512]; /* static to avoid large stack alloc */
|
||||
int modCount = 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f) && modCount < 512) {
|
||||
uint64_t start, end;
|
||||
char perms[8] = {}, path[512] = {};
|
||||
if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u %511[^\n]",
|
||||
&start, &end, perms, path) < 4)
|
||||
continue;
|
||||
|
||||
/* skip non-file / special mappings */
|
||||
/* trim leading whitespace from path */
|
||||
char* p = path;
|
||||
while (*p == ' ' || *p == '\t') ++p;
|
||||
if (*p != '/') continue;
|
||||
if (strncmp(p, "/dev/", 5) == 0) continue;
|
||||
if (strncmp(p, "/memfd:", 7) == 0) continue;
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < modCount; ++i) {
|
||||
if (strcmp(modules[i].path, p) == 0) {
|
||||
if (start < modules[i].base) modules[i].base = start;
|
||||
if (end > modules[i].end) modules[i].end = end;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
modules[modCount].base = start;
|
||||
modules[modCount].end = end;
|
||||
strncpy(modules[modCount].path, p, 511);
|
||||
modules[modCount].path[511] = '\0';
|
||||
++modCount;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
/* write entries + name strings into data region */
|
||||
uint32_t entryBytes = (uint32_t)(modCount * sizeof(RcxRpcModuleEntry));
|
||||
uint32_t nameDataOff = entryBytes;
|
||||
|
||||
for (int i = 0; i < modCount; ++i) {
|
||||
const char* basename = strrchr(modules[i].path, '/');
|
||||
basename = basename ? basename + 1 : modules[i].path;
|
||||
uint32_t nameLen = (uint32_t)strlen(basename);
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(
|
||||
data + (uint32_t)i * sizeof(RcxRpcModuleEntry));
|
||||
entry->base = modules[i].base;
|
||||
entry->size = modules[i].end - modules[i].base;
|
||||
entry->nameOffset = nameDataOff;
|
||||
entry->nameLength = nameLen;
|
||||
|
||||
if (nameDataOff + nameLen <= RCX_RPC_DATA_SIZE) {
|
||||
memcpy(data + nameDataOff, basename, nameLen);
|
||||
nameDataOff += nameLen;
|
||||
}
|
||||
}
|
||||
|
||||
hdr->responseCount = (uint32_t)modCount;
|
||||
hdr->totalDataUsed = nameDataOff;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
/* ── server thread ────────────────────────────────────────────────── */
|
||||
|
||||
static void* server_thread_func(void*)
|
||||
{
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
__atomic_store_n(&hdr->payloadReady, 1, __ATOMIC_RELEASE);
|
||||
|
||||
while (!__atomic_load_n(&g_shutdown, __ATOMIC_ACQUIRE)) {
|
||||
/* timed wait: 250ms */
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += 250000000;
|
||||
if (ts.tv_nsec >= 1000000000) {
|
||||
ts.tv_sec += 1;
|
||||
ts.tv_nsec -= 1000000000;
|
||||
}
|
||||
|
||||
int rc = sem_timedwait(g_reqSem, &ts);
|
||||
if (rc != 0) {
|
||||
if (errno == ETIMEDOUT) continue;
|
||||
break;
|
||||
}
|
||||
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
switch (static_cast<RcxRpcCommand>(hdr->command)) {
|
||||
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
|
||||
case RPC_CMD_WRITE: handle_write(hdr, data); break;
|
||||
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
|
||||
case RPC_CMD_PING: break;
|
||||
case RPC_CMD_SHUTDOWN:
|
||||
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
|
||||
break;
|
||||
default:
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
sem_post(g_rspSem);
|
||||
|
||||
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
|
||||
break;
|
||||
}
|
||||
|
||||
__atomic_store_n(&hdr->payloadReady, 0, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* ── init / cleanup ───────────────────────────────────────────────── */
|
||||
|
||||
static void payload_cleanup()
|
||||
{
|
||||
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
|
||||
|
||||
/* wake the thread if blocked */
|
||||
if (g_reqSem != SEM_FAILED) sem_post(g_reqSem);
|
||||
|
||||
if (__atomic_load_n(&g_threadRunning, __ATOMIC_ACQUIRE)) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += 2;
|
||||
pthread_timedjoin_np(g_thread, nullptr, &ts);
|
||||
}
|
||||
|
||||
if (g_mappedView && g_mappedView != MAP_FAILED) {
|
||||
munmap(g_mappedView, RCX_RPC_SHM_SIZE);
|
||||
g_mappedView = nullptr;
|
||||
}
|
||||
if (g_shmFd >= 0) { close(g_shmFd); g_shmFd = -1; }
|
||||
if (g_reqSem != SEM_FAILED) { sem_close(g_reqSem); g_reqSem = SEM_FAILED; }
|
||||
if (g_rspSem != SEM_FAILED) { sem_close(g_rspSem); g_rspSem = SEM_FAILED; }
|
||||
|
||||
/* unlink named objects */
|
||||
if (g_shmName[0]) shm_unlink(g_shmName);
|
||||
if (g_reqName[0]) sem_unlink(g_reqName);
|
||||
if (g_rspName[0]) sem_unlink(g_rspName);
|
||||
|
||||
if (g_memFd >= 0) { close(g_memFd); g_memFd = -1; }
|
||||
}
|
||||
|
||||
__attribute__((constructor))
|
||||
static void payload_init()
|
||||
{
|
||||
uint32_t pid = (uint32_t)getpid();
|
||||
|
||||
/* ── open /proc/self/mem for safe access ── */
|
||||
g_memFd = open("/proc/self/mem", O_RDWR);
|
||||
if (g_memFd < 0) return;
|
||||
|
||||
/* ── create main shared memory (PID-only naming) ── */
|
||||
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid);
|
||||
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid);
|
||||
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid);
|
||||
|
||||
g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600);
|
||||
if (g_shmFd < 0) return;
|
||||
if (ftruncate(g_shmFd, RCX_RPC_SHM_SIZE) != 0) {
|
||||
close(g_shmFd); g_shmFd = -1; return;
|
||||
}
|
||||
|
||||
g_mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, g_shmFd, 0);
|
||||
if (g_mappedView == MAP_FAILED) {
|
||||
g_mappedView = nullptr;
|
||||
close(g_shmFd); g_shmFd = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
hdr->version = RCX_RPC_VERSION;
|
||||
|
||||
/* image base from /proc/self/maps: first executable mapping */
|
||||
{
|
||||
FILE* f = fopen("/proc/self/maps", "r");
|
||||
if (f) {
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
uint64_t start;
|
||||
char perms[8] = {};
|
||||
if (sscanf(line, "%lx-%*x %7s", &start, perms) >= 2 && perms[2] == 'x') {
|
||||
hdr->imageBase = start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── create semaphores ── */
|
||||
g_reqSem = sem_open(g_reqName, O_CREAT, 0600, 0);
|
||||
g_rspSem = sem_open(g_rspName, O_CREAT, 0600, 0);
|
||||
if (g_reqSem == SEM_FAILED || g_rspSem == SEM_FAILED) {
|
||||
payload_cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── start server thread (it will set payloadReady = 1) ── */
|
||||
__atomic_store_n(&g_threadRunning, 1, __ATOMIC_RELEASE);
|
||||
if (pthread_create(&g_thread, nullptr, server_thread_func, nullptr) != 0) {
|
||||
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
|
||||
payload_cleanup();
|
||||
return;
|
||||
}
|
||||
pthread_detach(g_thread);
|
||||
}
|
||||
|
||||
__attribute__((destructor))
|
||||
static void payload_deinit()
|
||||
{
|
||||
payload_cleanup();
|
||||
}
|
||||
|
||||
#endif /* _WIN32 / linux */
|
||||
113
plugins/RemoteProcessMemory/rcx_rpc_protocol.h
Normal file
113
plugins/RemoteProcessMemory/rcx_rpc_protocol.h
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* RCX RPC Protocol -- shared between plugin DLL and payload DLL/SO.
|
||||
* No dependencies beyond standard C headers.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── constants ─────────────────────────────────────────────────────── */
|
||||
#define RCX_RPC_VERSION 1
|
||||
#define RCX_RPC_MAX_BATCH 256
|
||||
#define RCX_RPC_SHM_SIZE (1024 * 1024) /* 1 MB */
|
||||
#define RCX_RPC_HEADER_SIZE 4096
|
||||
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
|
||||
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
|
||||
|
||||
/* status codes */
|
||||
#define RCX_RPC_STATUS_OK 0
|
||||
#define RCX_RPC_STATUS_ERROR 1
|
||||
#define RCX_RPC_STATUS_PARTIAL 2
|
||||
|
||||
/* ── commands ──────────────────────────────────────────────────────── */
|
||||
#ifdef __cplusplus
|
||||
enum RcxRpcCommand : uint32_t {
|
||||
#else
|
||||
typedef uint32_t RcxRpcCommand;
|
||||
enum {
|
||||
#endif
|
||||
RPC_CMD_NONE = 0,
|
||||
RPC_CMD_READ_BATCH = 1, /* batch read: N {address, length} pairs */
|
||||
RPC_CMD_WRITE = 2, /* single write */
|
||||
RPC_CMD_ENUM_MODULES = 3, /* enumerate loaded modules */
|
||||
RPC_CMD_PING = 4, /* heartbeat */
|
||||
RPC_CMD_SHUTDOWN = 5, /* graceful teardown */
|
||||
};
|
||||
|
||||
/* ── wire structs (natural alignment, verified by static_assert) ─── */
|
||||
|
||||
struct RcxRpcReadEntry {
|
||||
uint64_t address;
|
||||
uint32_t length;
|
||||
uint32_t dataOffset; /* offset into data region for response bytes */
|
||||
};
|
||||
|
||||
struct RcxRpcModuleEntry {
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
uint32_t nameOffset; /* offset into data region, UTF-16 on Win, UTF-8 on Linux */
|
||||
uint32_t nameLength; /* in bytes */
|
||||
};
|
||||
|
||||
/*
|
||||
* Header -- lives at shared-memory offset 0, padded to 4096 bytes.
|
||||
*
|
||||
* offset field
|
||||
* ------ -----
|
||||
* 0 version (4)
|
||||
* 4 payloadReady (4)
|
||||
* 8 command (4)
|
||||
* 12 requestCount (4)
|
||||
* 16 writeAddress (8)
|
||||
* 24 writeLength (4)
|
||||
* 28 status (4)
|
||||
* 32 responseCount (4)
|
||||
* 36 totalDataUsed (4)
|
||||
* 40 imageBase (8) -- main module base from PEB / procfs
|
||||
* 48 _pad[4048]
|
||||
*/
|
||||
struct RcxRpcHeader {
|
||||
uint32_t version;
|
||||
uint32_t payloadReady; /* payload sets to 1 after init */
|
||||
uint32_t command; /* RcxRpcCommand */
|
||||
uint32_t requestCount;
|
||||
uint64_t writeAddress;
|
||||
uint32_t writeLength;
|
||||
uint32_t status; /* RCX_RPC_STATUS_* */
|
||||
uint32_t responseCount;
|
||||
uint32_t totalDataUsed;
|
||||
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
|
||||
uint8_t _pad[RCX_RPC_HEADER_SIZE - 48];
|
||||
};
|
||||
|
||||
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
|
||||
|
||||
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_shm_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_req_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_rsp_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
|
||||
#endif
|
||||
593
plugins/RemoteProcessMemory/tests/test_rpc_client.cpp
Normal file
593
plugins/RemoteProcessMemory/tests/test_rpc_client.cpp
Normal file
@@ -0,0 +1,593 @@
|
||||
/*
|
||||
* test_rpc_client -- connects to a running test_rpc_host (or spawns one),
|
||||
* exercises every RPC command, and benchmarks throughput.
|
||||
*
|
||||
* Usage:
|
||||
* test_rpc_client (auto-spawn host)
|
||||
* test_rpc_client <pid> [testbuf_hex testlen]
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
#include <chrono>
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <semaphore.h>
|
||||
# include <libgen.h>
|
||||
# include <limits.h>
|
||||
#endif
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Minimal standalone IPC client (no Qt, mirrors plugin's IpcClient)
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
struct TestIpcClient {
|
||||
#ifdef _WIN32
|
||||
HANDLE hShm = nullptr;
|
||||
HANDLE hReqEvent = nullptr;
|
||||
HANDLE hRspEvent = nullptr;
|
||||
#else
|
||||
int shmFd = -1;
|
||||
sem_t* reqSem = SEM_FAILED;
|
||||
sem_t* rspSem = SEM_FAILED;
|
||||
#endif
|
||||
void* view = nullptr;
|
||||
bool ok = false;
|
||||
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs;
|
||||
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(10);
|
||||
}
|
||||
view = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!view) { CloseHandle(hShm); hShm = nullptr; return false; }
|
||||
|
||||
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
|
||||
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
|
||||
if (!hReqEvent || !hRspEvent) return false;
|
||||
#else
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
shmFd = shm_open(shmName, O_RDWR, 0);
|
||||
if (shmFd >= 0) break;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(10000);
|
||||
}
|
||||
view = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shmFd, 0);
|
||||
if (view == MAP_FAILED) { view = nullptr; close(shmFd); shmFd = -1; return false; }
|
||||
|
||||
reqSem = sem_open(reqName, 0);
|
||||
rspSem = sem_open(rspName, 0);
|
||||
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) return false;
|
||||
#endif
|
||||
/* wait for payloadReady */
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
#ifdef _WIN32
|
||||
while (!hdr->payloadReady) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(5);
|
||||
}
|
||||
#else
|
||||
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (view) { UnmapViewOfFile(view); view = nullptr; }
|
||||
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
|
||||
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
|
||||
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
|
||||
#else
|
||||
if (view) { munmap(view, RCX_RPC_SHM_SIZE); view = nullptr; }
|
||||
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
|
||||
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
|
||||
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
|
||||
#endif
|
||||
ok = false;
|
||||
}
|
||||
|
||||
bool signalAndWait(int timeoutMs = 2000)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
SetEvent(hReqEvent);
|
||||
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
|
||||
#else
|
||||
sem_post(reqSem);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += timeoutMs / 1000;
|
||||
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
return sem_timedwait(rspSem, &ts) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── RPC helpers ──────────────────────────────────────────────── */
|
||||
|
||||
bool rpc_ping()
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
hdr->command = RPC_CMD_PING;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
return signalAndWait();
|
||||
}
|
||||
|
||||
bool rpc_read(uint64_t addr, void* buf, uint32_t len)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = 1;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
auto* entry = (RcxRpcReadEntry*)data;
|
||||
entry->address = addr;
|
||||
entry->length = len;
|
||||
entry->dataOffset = sizeof(RcxRpcReadEntry);
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
memcpy(buf, data + entry->dataOffset, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rpc_read_batch(const uint64_t* addrs, const uint32_t* lens,
|
||||
uint32_t count, uint8_t* outBuf)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = count;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
/* lay out entries, then data offsets after all entries */
|
||||
uint32_t entriesSize = count * (uint32_t)sizeof(RcxRpcReadEntry);
|
||||
uint32_t dataOff = entriesSize;
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
|
||||
e->address = addrs[i];
|
||||
e->length = lens[i];
|
||||
e->dataOffset = dataOff;
|
||||
dataOff += lens[i];
|
||||
}
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
|
||||
/* copy out response data */
|
||||
uint32_t off = 0;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
|
||||
memcpy(outBuf + off, data + e->dataOffset, e->length);
|
||||
off += e->length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rpc_write(uint64_t addr, const void* buf, uint32_t len)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_WRITE;
|
||||
hdr->writeAddress = addr;
|
||||
hdr->writeLength = len;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
memcpy(data, buf, len);
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
return hdr->status == RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
struct ModInfo { uint64_t base; uint64_t size; char name[256]; };
|
||||
|
||||
int rpc_enum_modules(ModInfo* out, int maxOut)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_ENUM_MODULES;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) return -1;
|
||||
if (hdr->status != RCX_RPC_STATUS_OK) return -1;
|
||||
|
||||
int count = (int)hdr->responseCount;
|
||||
if (count > maxOut) count = maxOut;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto* entry = (RcxRpcModuleEntry*)(data + i * sizeof(RcxRpcModuleEntry));
|
||||
out[i].base = entry->base;
|
||||
out[i].size = entry->size;
|
||||
#ifdef _WIN32
|
||||
/* names are UTF-16 on Windows */
|
||||
int wchars = (int)(entry->nameLength / sizeof(wchar_t));
|
||||
WideCharToMultiByte(CP_UTF8, 0,
|
||||
(const wchar_t*)(data + entry->nameOffset), wchars,
|
||||
out[i].name, 255, nullptr, nullptr);
|
||||
out[i].name[255] = '\0';
|
||||
#else
|
||||
int nLen = (int)entry->nameLength;
|
||||
if (nLen > 255) nLen = 255;
|
||||
memcpy(out[i].name, data + entry->nameOffset, nLen);
|
||||
out[i].name[nLen] = '\0';
|
||||
#endif
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void rpc_shutdown()
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
hdr->command = RPC_CMD_SHUTDOWN;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
signalAndWait(500);
|
||||
}
|
||||
};
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Auto-spawn host
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
#ifdef _WIN32
|
||||
static HANDLE g_hostProcess = nullptr;
|
||||
#else
|
||||
static pid_t g_hostPid = 0;
|
||||
#endif
|
||||
static FILE* g_hostPipe = nullptr;
|
||||
|
||||
static bool spawn_host(uint32_t* outPid,
|
||||
uint64_t* outTestBuf, uint32_t* outTestLen)
|
||||
{
|
||||
/* resolve path to test_rpc_host next to ourselves */
|
||||
char cmd[2048];
|
||||
#ifdef _WIN32
|
||||
char exePath[MAX_PATH];
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
char* slash = strrchr(exePath, '\\');
|
||||
if (!slash) slash = strrchr(exePath, '/');
|
||||
if (slash) *(slash + 1) = '\0';
|
||||
snprintf(cmd, sizeof(cmd), "\"%stest_rpc_host.exe\" autotest", exePath);
|
||||
g_hostPipe = _popen(cmd, "r");
|
||||
#else
|
||||
char exePath[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
|
||||
if (n <= 0) return false;
|
||||
exePath[n] = '\0';
|
||||
char* dir = dirname(exePath);
|
||||
snprintf(cmd, sizeof(cmd), "%s/test_rpc_host autotest", dir);
|
||||
g_hostPipe = popen(cmd, "r");
|
||||
#endif
|
||||
if (!g_hostPipe) {
|
||||
fprintf(stderr, "ERROR: cannot spawn host: %s\n", cmd);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* read READY line */
|
||||
char line[512];
|
||||
if (!fgets(line, sizeof(line), g_hostPipe)) {
|
||||
fprintf(stderr, "ERROR: no output from host\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* parse: READY pid=X testbuf=0xZ testlen=N */
|
||||
unsigned long long tbuf = 0;
|
||||
unsigned tlen = 0;
|
||||
if (sscanf(line, "READY pid=%u testbuf=0x%llx testlen=%u",
|
||||
outPid, &tbuf, &tlen) < 1) {
|
||||
fprintf(stderr, "ERROR: cannot parse host output: %s\n", line);
|
||||
return false;
|
||||
}
|
||||
*outTestBuf = (uint64_t)tbuf;
|
||||
*outTestLen = (uint32_t)tlen;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cleanup_host()
|
||||
{
|
||||
if (g_hostPipe) {
|
||||
#ifdef _WIN32
|
||||
_pclose(g_hostPipe);
|
||||
#else
|
||||
pclose(g_hostPipe);
|
||||
#endif
|
||||
g_hostPipe = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Printing helpers
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static void print_pass(const char* name) { printf(" [PASS] %s\n", name); }
|
||||
static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* main
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
uint32_t pid = 0;
|
||||
uint64_t testBuf = 0;
|
||||
uint32_t testLen = 0;
|
||||
bool autoMode = false;
|
||||
|
||||
if (argc >= 2) {
|
||||
pid = (uint32_t)atoi(argv[1]);
|
||||
if (argc >= 4) {
|
||||
testBuf = (uint64_t)strtoull(argv[2], nullptr, 0);
|
||||
testLen = (uint32_t)atoi(argv[3]);
|
||||
}
|
||||
} else {
|
||||
autoMode = true;
|
||||
printf("Auto-spawning test_rpc_host...\n");
|
||||
if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to PID=%u testbuf=0x%llx testlen=%u\n\n",
|
||||
pid, (unsigned long long)testBuf, testLen);
|
||||
|
||||
/* ── connect ── */
|
||||
TestIpcClient ipc;
|
||||
if (!ipc.connect(pid)) {
|
||||
fprintf(stderr, "ERROR: IPC connect failed\n");
|
||||
if (autoMode) cleanup_host();
|
||||
return 1;
|
||||
}
|
||||
printf("=== Functional Tests ===\n");
|
||||
|
||||
/* ── test: ping ── */
|
||||
if (ipc.rpc_ping()) print_pass("Ping");
|
||||
else print_fail("Ping");
|
||||
|
||||
/* ── test: enumerate modules ── */
|
||||
TestIpcClient::ModInfo mods[512];
|
||||
int modCount = ipc.rpc_enum_modules(mods, 512);
|
||||
if (modCount > 0) {
|
||||
printf(" [PASS] EnumModules (%d modules)\n", modCount);
|
||||
printf(" first: %s base=0x%llx size=0x%llx\n",
|
||||
mods[0].name,
|
||||
(unsigned long long)mods[0].base,
|
||||
(unsigned long long)mods[0].size);
|
||||
} else {
|
||||
print_fail("EnumModules");
|
||||
}
|
||||
|
||||
/* ── test: read module header (MZ / ELF magic) ── */
|
||||
if (modCount > 0) {
|
||||
uint8_t header[4] = {};
|
||||
if (ipc.rpc_read(mods[0].base, header, 4)) {
|
||||
#ifdef _WIN32
|
||||
if (header[0] == 'M' && header[1] == 'Z')
|
||||
print_pass("ReadModuleHeader (MZ)");
|
||||
else
|
||||
print_fail("ReadModuleHeader (expected MZ)");
|
||||
#else
|
||||
if (header[0] == 0x7F && header[1] == 'E' &&
|
||||
header[2] == 'L' && header[3] == 'F')
|
||||
print_pass("ReadModuleHeader (ELF)");
|
||||
else
|
||||
print_fail("ReadModuleHeader (expected ELF)");
|
||||
#endif
|
||||
} else {
|
||||
print_fail("ReadModuleHeader (read failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: read test buffer (known pattern) ── */
|
||||
if (testBuf && testLen >= 4096) {
|
||||
uint8_t buf[4096];
|
||||
if (ipc.rpc_read(testBuf, buf, 4096)) {
|
||||
bool good = true;
|
||||
for (int i = 0; i < 4096; ++i) {
|
||||
if (buf[i] != (uint8_t)(i & 0xFF)) { good = false; break; }
|
||||
}
|
||||
if (good) print_pass("ReadTestBuffer (4096 bytes, pattern verified)");
|
||||
else print_fail("ReadTestBuffer (pattern mismatch)");
|
||||
} else {
|
||||
print_fail("ReadTestBuffer (read failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: write ── */
|
||||
if (testBuf && testLen >= 16) {
|
||||
uint8_t patch[4] = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
if (ipc.rpc_write(testBuf, patch, 4)) {
|
||||
uint8_t verify[4] = {};
|
||||
ipc.rpc_read(testBuf, verify, 4);
|
||||
if (memcmp(verify, patch, 4) == 0)
|
||||
print_pass("Write + ReadBack (0xDEADBEEF)");
|
||||
else
|
||||
print_fail("Write + ReadBack (readback mismatch)");
|
||||
} else {
|
||||
print_fail("Write (write failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: batch read ── */
|
||||
if (testBuf && testLen >= 8192) {
|
||||
const uint32_t N = 4;
|
||||
uint64_t addrs[N];
|
||||
uint32_t lens[N];
|
||||
for (uint32_t i = 0; i < N; ++i) {
|
||||
addrs[i] = testBuf + i * 1024;
|
||||
lens[i] = 1024;
|
||||
}
|
||||
uint8_t out[4096];
|
||||
if (ipc.rpc_read_batch(addrs, lens, N, out)) {
|
||||
print_pass("BatchRead (4 x 1024 bytes)");
|
||||
} else {
|
||||
print_fail("BatchRead");
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Benchmarks ===\n");
|
||||
|
||||
/* choose a valid address for benchmarking */
|
||||
uint64_t benchAddr = testBuf ? testBuf : (modCount > 0 ? mods[0].base : 0);
|
||||
if (!benchAddr) {
|
||||
printf(" (no valid address for benchmarks, skipping)\n");
|
||||
} else {
|
||||
|
||||
/* ── benchmark: single 4 KB reads ── */
|
||||
{
|
||||
const int ITERS = 10000;
|
||||
const int PAGE = 4096;
|
||||
uint8_t tmp[4096];
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read(benchAddr, tmp, PAGE);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Single 4 KB reads:\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/read\n", us / ITERS);
|
||||
}
|
||||
|
||||
/* ── benchmark: single 64 B reads (pointer-chase-size) ── */
|
||||
{
|
||||
const int ITERS = 50000;
|
||||
const int SZ = 64;
|
||||
uint8_t tmp[64];
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read(benchAddr, tmp, SZ);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalKB = (double)ITERS * SZ / 1024.0;
|
||||
|
||||
printf(" Single 64 B reads (pointer-chase):\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f KB\n", totalKB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f KB/s\n", totalKB / secs);
|
||||
printf(" Avg latency: %.2f us/read\n", us / ITERS);
|
||||
}
|
||||
|
||||
/* ── benchmark: batch read (50 x 4 KB, simulating refresh) ── */
|
||||
{
|
||||
const int ITERS = 2000;
|
||||
const uint32_t BATCH = 50;
|
||||
const uint32_t PAGE = 4096;
|
||||
|
||||
uint64_t addrs[BATCH];
|
||||
uint32_t lens[BATCH];
|
||||
for (uint32_t i = 0; i < BATCH; ++i) {
|
||||
/* wrap within test buffer or module */
|
||||
addrs[i] = benchAddr + (i * PAGE) % 65536;
|
||||
lens[i] = PAGE;
|
||||
}
|
||||
|
||||
/* allocate response buffer */
|
||||
uint8_t* outBuf = (uint8_t*)malloc(BATCH * PAGE);
|
||||
if (!outBuf) {
|
||||
printf(" (batch malloc failed, skipping)\n");
|
||||
} else {
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read_batch(addrs, lens, BATCH, outBuf);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * BATCH * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Batch read (%u x %u B, simulating refresh):\n", BATCH, PAGE);
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/batch\n", us / ITERS);
|
||||
printf(" Per-page : %.2f us/page\n", us / (ITERS * BATCH));
|
||||
|
||||
free(outBuf);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── benchmark: write 4 KB ── */
|
||||
if (testBuf && testLen >= 4096) {
|
||||
const int ITERS = 10000;
|
||||
const int PAGE = 4096;
|
||||
uint8_t tmp[4096];
|
||||
memset(tmp, 0x42, sizeof(tmp));
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_write(testBuf, tmp, PAGE);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Write 4 KB:\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/write\n", us / ITERS);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── shutdown ── */
|
||||
printf("\nSending shutdown...\n");
|
||||
ipc.rpc_shutdown();
|
||||
ipc.disconnect();
|
||||
|
||||
if (autoMode) {
|
||||
/* wait for host to exit */
|
||||
#ifdef _WIN32
|
||||
Sleep(500);
|
||||
#else
|
||||
usleep(500000);
|
||||
#endif
|
||||
cleanup_host();
|
||||
}
|
||||
|
||||
printf("Done.\n");
|
||||
return 0;
|
||||
}
|
||||
187
plugins/RemoteProcessMemory/tests/test_rpc_host.cpp
Normal file
187
plugins/RemoteProcessMemory/tests/test_rpc_host.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
|
||||
*
|
||||
* Usage: test_rpc_host
|
||||
*
|
||||
* Prints a READY line (machine-parseable), then waits for the payload
|
||||
* to shut down (RPC_CMD_SHUTDOWN from the client).
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <dlfcn.h>
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <semaphore.h>
|
||||
# include <libgen.h>
|
||||
# include <limits.h>
|
||||
#endif
|
||||
|
||||
/* ── Helpers ──────────────────────────────────────────────────────── */
|
||||
|
||||
static uint32_t current_pid()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return (uint32_t)GetCurrentProcessId();
|
||||
#else
|
||||
return (uint32_t)getpid();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void sleep_ms(int ms)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
Sleep((DWORD)ms);
|
||||
#else
|
||||
usleep((useconds_t)ms * 1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Resolve payload path relative to this executable */
|
||||
static int payload_path(char* out, int outLen)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char exePath[MAX_PATH];
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
char* slash = strrchr(exePath, '\\');
|
||||
if (!slash) slash = strrchr(exePath, '/');
|
||||
if (slash) *(slash + 1) = '\0';
|
||||
snprintf(out, outLen, "%srcx_payload.dll", exePath);
|
||||
#else
|
||||
char exePath[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
|
||||
if (n <= 0) return -1;
|
||||
exePath[n] = '\0';
|
||||
char* dir = dirname(exePath);
|
||||
snprintf(out, outLen, "%s/rcx_payload.so", dir);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Open the main shared memory (read-only, just to monitor payloadReady) */
|
||||
static void* open_main_shm(uint32_t pid)
|
||||
{
|
||||
char shmName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE h = nullptr;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
h = OpenFileMappingA(FILE_MAP_READ, FALSE, shmName);
|
||||
if (h) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (!h) return nullptr;
|
||||
void* v = MapViewOfFile(h, FILE_MAP_READ, 0, 0, sizeof(RcxRpcHeader));
|
||||
return v;
|
||||
#else
|
||||
int fd = -1;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
fd = shm_open(shmName, O_RDONLY, 0);
|
||||
if (fd >= 0) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (fd < 0) return nullptr;
|
||||
void* v = mmap(nullptr, sizeof(RcxRpcHeader), PROT_READ, MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
return (v == MAP_FAILED) ? nullptr : v;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── Test buffer (known pattern for client to verify reads/writes) ── */
|
||||
static uint8_t g_testBuf[65536];
|
||||
|
||||
/* ── main ─────────────────────────────────────────────────────────── */
|
||||
|
||||
int main(int, char**)
|
||||
{
|
||||
uint32_t pid = current_pid();
|
||||
|
||||
/* fill test buffer with known pattern */
|
||||
for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
|
||||
g_testBuf[i] = (uint8_t)(i & 0xFF);
|
||||
|
||||
/* load payload */
|
||||
char plPath[1024];
|
||||
if (payload_path(plPath, sizeof(plPath)) != 0) {
|
||||
fprintf(stderr, "ERROR: cannot determine payload path\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HMODULE hPayload = LoadLibraryA(plPath);
|
||||
if (!hPayload) {
|
||||
fprintf(stderr, "ERROR: LoadLibrary(%s) failed (%lu)\n",
|
||||
plPath, GetLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Call RcxPayloadInit() — DllMain is minimal, init must be explicit */
|
||||
typedef bool (*RcxPayloadInitFn)();
|
||||
auto pfnInit = (RcxPayloadInitFn)GetProcAddress(hPayload, "RcxPayloadInit");
|
||||
if (!pfnInit || !pfnInit()) {
|
||||
fprintf(stderr, "ERROR: RcxPayloadInit() failed or not found\n");
|
||||
FreeLibrary(hPayload);
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
void* hPayload = dlopen(plPath, RTLD_NOW);
|
||||
if (!hPayload) {
|
||||
fprintf(stderr, "ERROR: dlopen(%s): %s\n", plPath, dlerror());
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* open main shm and wait for payloadReady */
|
||||
void* shmView = open_main_shm(pid);
|
||||
if (!shmView) {
|
||||
fprintf(stderr, "ERROR: failed to open main shared memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
RcxRpcHeader* hdr = (RcxRpcHeader*)shmView;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
if (hdr->payloadReady) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (!hdr->payloadReady) {
|
||||
fprintf(stderr, "ERROR: payload did not become ready\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* print READY line for the client to parse */
|
||||
printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
|
||||
pid,
|
||||
(unsigned long long)(uintptr_t)g_testBuf,
|
||||
(unsigned)sizeof(g_testBuf));
|
||||
fflush(stdout);
|
||||
|
||||
/* wait until payload shuts down */
|
||||
while (hdr->payloadReady)
|
||||
sleep_ms(100);
|
||||
|
||||
printf("Payload shut down, exiting.\n");
|
||||
|
||||
#ifdef _WIN32
|
||||
/* give the timer queue a moment to drain */
|
||||
Sleep(200);
|
||||
FreeLibrary(hPayload);
|
||||
if (shmView) UnmapViewOfFile(shmView);
|
||||
#else
|
||||
usleep(200000);
|
||||
dlclose(hPayload);
|
||||
if (shmView) munmap(shmView, sizeof(RcxRpcHeader));
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -197,53 +197,15 @@ void WinDbgMemoryProvider::querySessionInfo()
|
||||
}
|
||||
}
|
||||
|
||||
if (m_symbols) {
|
||||
ULONG numModules = 0, numUnloaded = 0;
|
||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
||||
qDebug() << "[WinDbg] GetNumberModules hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "loaded=" << numModules << "unloaded=" << numUnloaded;
|
||||
if (SUCCEEDED(hr) && numModules > 0) {
|
||||
char modName[256] = {};
|
||||
ULONG modSize = 0;
|
||||
hr = m_symbols->GetModuleNames(0, 0, nullptr, 0, nullptr,
|
||||
modName, sizeof(modName), &modSize,
|
||||
nullptr, 0, nullptr);
|
||||
if (SUCCEEDED(hr) && modSize > 0)
|
||||
m_name = QString::fromUtf8(modName);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_name.isEmpty())
|
||||
m_name = m_isLive ? QStringLiteral("DbgEng (Live)") : QStringLiteral("DbgEng (Dump)");
|
||||
|
||||
if (m_symbols) {
|
||||
ULONG numModules = 0, numUnloaded = 0;
|
||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
||||
if (SUCCEEDED(hr) && numModules > 0) {
|
||||
ULONG64 moduleBase = 0;
|
||||
hr = m_symbols->GetModuleByIndex(0, &moduleBase);
|
||||
qDebug() << "[WinDbg] Module 0 base=" << Qt::hex << moduleBase;
|
||||
if (SUCCEEDED(hr))
|
||||
m_base = moduleBase;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_base && m_dataSpaces) {
|
||||
uint8_t probe[2] = {};
|
||||
ULONG got = 0;
|
||||
hr = m_dataSpaces->ReadVirtual(m_base, probe, 2, &got);
|
||||
qDebug() << "[WinDbg] Probe read at" << Qt::hex << m_base
|
||||
<< "hr=" << (unsigned long)hr << "got=" << got
|
||||
<< "bytes:" << (int)probe[0] << (int)probe[1];
|
||||
if (FAILED(hr) || got == 0) {
|
||||
qWarning() << "[WinDbg] Probe read FAILED — cleaning up";
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// WinDbg provides access to the entire virtual address space.
|
||||
// Do NOT auto-select a module as base — let the user set their
|
||||
// own base address. m_base stays 0 so the controller won't
|
||||
// override tree.baseAddress.
|
||||
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
|
||||
: QStringLiteral("WinDbg (Dump)");
|
||||
|
||||
qDebug() << "[WinDbg] Ready. name=" << m_name
|
||||
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive;
|
||||
<< "isLive=" << m_isLive;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -304,9 +266,19 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool result = false;
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesRead = 0;
|
||||
HRESULT hr = m_dataSpaces->ReadVirtual(m_base + addr, buf, (ULONG)len, &bytesRead);
|
||||
if (FAILED(hr) || (int)bytesRead < len)
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
||||
if (SUCCEEDED(hr) && (int)bytesRead >= len) {
|
||||
result = true;
|
||||
return;
|
||||
}
|
||||
// Partial or failed read — zero-fill remainder and log
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
++m_readFailCount;
|
||||
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
|
||||
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
|
||||
<< "len=" << Qt::dec << len
|
||||
<< "hr=0x" << Qt::hex << (unsigned long)hr
|
||||
<< "got=" << Qt::dec << bytesRead;
|
||||
result = bytesRead > 0;
|
||||
});
|
||||
return result;
|
||||
@@ -324,7 +296,7 @@ bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool result = false;
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesWritten = 0;
|
||||
HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast<void*>(buf),
|
||||
HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
|
||||
(ULONG)len, &bytesWritten);
|
||||
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
|
||||
});
|
||||
@@ -364,7 +336,7 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
|
||||
char nameBuf[512] = {};
|
||||
ULONG nameSize = 0;
|
||||
ULONG64 displacement = 0;
|
||||
HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf),
|
||||
HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
|
||||
&nameSize, &displacement);
|
||||
if (SUCCEEDED(hr) && nameSize > 0) {
|
||||
result = QString::fromUtf8(nameBuf);
|
||||
|
||||
@@ -62,7 +62,6 @@ public:
|
||||
|
||||
bool isLive() const override { return m_isLive; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
void setBase(uint64_t b) override { m_base = b; }
|
||||
|
||||
private:
|
||||
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
||||
@@ -84,6 +83,7 @@ private:
|
||||
bool m_isLive = false;
|
||||
bool m_writable = false;
|
||||
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
||||
mutable int m_readFailCount = 0;
|
||||
|
||||
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
|
||||
// transport is thread-affine — all calls must happen on the thread
|
||||
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB |
300
src/addressparser.cpp
Normal file
300
src/addressparser.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#include "addressparser.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Address Expression Parser ──────────────────────────────────────────
|
||||
//
|
||||
// Parses expressions like:
|
||||
// "7FF66CCE0000" → plain hex address
|
||||
// "0x100 + 0x200" → arithmetic on hex values
|
||||
// "<Program.exe> + 0xDE" → module base + offset
|
||||
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
|
||||
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
|
||||
//
|
||||
// Grammar (standard operator precedence: *, / bind tighter than +, -):
|
||||
//
|
||||
// expr = term (('+' | '-') term)*
|
||||
// term = unary (('*' | '/') unary)*
|
||||
// unary = '-' unary | atom
|
||||
// atom = '[' expr ']' -- read pointer at address (dereference)
|
||||
// | '<' moduleName '>' -- resolve module base address
|
||||
// | '(' expr ')' -- grouping
|
||||
// | hexLiteral -- hex number, optional 0x prefix
|
||||
//
|
||||
// All numeric literals are hexadecimal (base 16).
|
||||
// Module names and pointer reads are resolved via optional callbacks.
|
||||
// Without callbacks, modules and dereferences evaluate to 0 (syntax-check mode).
|
||||
|
||||
class ExpressionParser {
|
||||
public:
|
||||
ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks)
|
||||
: m_input(input), m_callbacks(callbacks) {}
|
||||
|
||||
AddressParseResult parse() {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return error("empty expression");
|
||||
|
||||
uint64_t value = 0;
|
||||
if (!parseExpression(value))
|
||||
return error(m_error);
|
||||
|
||||
skipSpaces();
|
||||
if (!atEnd())
|
||||
return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos]));
|
||||
|
||||
return {true, value, {}, -1};
|
||||
}
|
||||
|
||||
private:
|
||||
const QString& m_input;
|
||||
const AddressParserCallbacks* m_callbacks;
|
||||
int m_pos = 0;
|
||||
QString m_error;
|
||||
int m_errorPos = 0;
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
bool atEnd() const { return m_pos >= m_input.size(); }
|
||||
|
||||
QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; }
|
||||
|
||||
void advance() { m_pos++; }
|
||||
|
||||
void skipSpaces() {
|
||||
while (!atEnd() && m_input[m_pos].isSpace())
|
||||
m_pos++;
|
||||
}
|
||||
|
||||
AddressParseResult error(const QString& msg) const {
|
||||
return {false, 0, msg, m_errorPos};
|
||||
}
|
||||
|
||||
bool fail(const QString& msg) {
|
||||
m_error = msg;
|
||||
m_errorPos = m_pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool expect(QChar ch) {
|
||||
skipSpaces();
|
||||
if (peek() != ch)
|
||||
return fail(QStringLiteral("expected '%1'").arg(ch));
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isHexDigit(QChar ch) {
|
||||
return (ch >= '0' && ch <= '9')
|
||||
|| (ch >= 'a' && ch <= 'f')
|
||||
|| (ch >= 'A' && ch <= 'F');
|
||||
}
|
||||
|
||||
// ── Recursive descent parsing ──
|
||||
|
||||
// expr = term (('+' | '-') term)*
|
||||
bool parseExpression(uint64_t& result) {
|
||||
if (!parseTerm(result))
|
||||
return false;
|
||||
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar op = peek();
|
||||
if (op != '+' && op != '-')
|
||||
break;
|
||||
advance();
|
||||
|
||||
uint64_t rhs = 0;
|
||||
if (!parseTerm(rhs))
|
||||
return false;
|
||||
|
||||
result = (op == '+') ? result + rhs : result - rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// term = unary (('*' | '/') unary)*
|
||||
bool parseTerm(uint64_t& result) {
|
||||
if (!parseUnary(result))
|
||||
return false;
|
||||
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar op = peek();
|
||||
if (op != '*' && op != '/')
|
||||
break;
|
||||
advance();
|
||||
|
||||
uint64_t rhs = 0;
|
||||
if (!parseUnary(rhs))
|
||||
return false;
|
||||
|
||||
if (op == '*') {
|
||||
result *= rhs;
|
||||
} else {
|
||||
if (rhs == 0)
|
||||
return fail("division by zero");
|
||||
result /= rhs;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// unary = '-' unary | atom
|
||||
bool parseUnary(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (peek() == '-') {
|
||||
advance();
|
||||
uint64_t inner = 0;
|
||||
if (!parseUnary(inner))
|
||||
return false;
|
||||
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
|
||||
return true;
|
||||
}
|
||||
return parseAtom(result);
|
||||
}
|
||||
|
||||
// atom = '[' expr ']' | '<' name '>' | '(' expr ')' | hexLiteral
|
||||
bool parseAtom(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return fail("unexpected end of expression");
|
||||
|
||||
QChar ch = peek();
|
||||
|
||||
if (ch == '[') return parseDereference(result);
|
||||
if (ch == '<') return parseModuleName(result);
|
||||
if (ch == '(') return parseGrouping(result);
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// '[' expr ']' — read the pointer value at the computed address
|
||||
bool parseDereference(uint64_t& result) {
|
||||
advance(); // skip '['
|
||||
|
||||
uint64_t address = 0;
|
||||
if (!parseExpression(address))
|
||||
return false;
|
||||
if (!expect(']'))
|
||||
return false;
|
||||
|
||||
// Without a callback, just return 0 (syntax-check mode)
|
||||
if (!m_callbacks || !m_callbacks->readPointer) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->readPointer(address, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '<' moduleName '>' — resolve a module's base address (e.g. <Program.exe>)
|
||||
bool parseModuleName(uint64_t& result) {
|
||||
advance(); // skip '<'
|
||||
|
||||
int nameStart = m_pos;
|
||||
while (!atEnd() && peek() != '>')
|
||||
advance();
|
||||
if (atEnd())
|
||||
return fail("expected '>'");
|
||||
|
||||
QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed();
|
||||
advance(); // skip '>'
|
||||
|
||||
if (name.isEmpty())
|
||||
return fail("empty module name");
|
||||
|
||||
// Without a callback, just return 0 (syntax-check mode)
|
||||
if (!m_callbacks || !m_callbacks->resolveModule) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->resolveModule(name, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("module '%1' not found").arg(name));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '(' expr ')' — parenthesized sub-expression for grouping
|
||||
bool parseGrouping(uint64_t& result) {
|
||||
advance(); // skip '('
|
||||
if (!parseExpression(result))
|
||||
return false;
|
||||
return expect(')');
|
||||
}
|
||||
|
||||
// Hex number with optional "0x" prefix. All literals are base-16.
|
||||
bool parseHexNumber(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return fail("unexpected end of expression");
|
||||
|
||||
int start = m_pos;
|
||||
|
||||
// Skip optional 0x/0X prefix
|
||||
if (m_pos + 1 < m_input.size()
|
||||
&& m_input[m_pos] == '0'
|
||||
&& (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X'))
|
||||
m_pos += 2;
|
||||
|
||||
// Consume hex digits
|
||||
int digitsStart = m_pos;
|
||||
while (!atEnd() && isHexDigit(peek()))
|
||||
advance();
|
||||
|
||||
if (m_pos == digitsStart) {
|
||||
m_errorPos = start;
|
||||
return fail("expected hex number");
|
||||
}
|
||||
|
||||
QString digits = m_input.mid(digitsStart, m_pos - digitsStart);
|
||||
bool ok = false;
|
||||
result = digits.toULongLong(&ok, 16);
|
||||
if (!ok) {
|
||||
m_errorPos = start;
|
||||
return fail("invalid hex number");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────
|
||||
|
||||
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
|
||||
const AddressParserCallbacks* cb)
|
||||
{
|
||||
Q_UNUSED(ptrSize);
|
||||
|
||||
// WinDbg displays 64-bit addresses with backtick separators for readability,
|
||||
// e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly.
|
||||
// Also remove ' in case user uses it
|
||||
QString cleaned = formula;
|
||||
cleaned.remove('`');
|
||||
cleaned.remove('\'');
|
||||
|
||||
ExpressionParser parser(cleaned, cb);
|
||||
return parser.parse();
|
||||
}
|
||||
|
||||
QString AddressParser::validate(const QString& formula)
|
||||
{
|
||||
QString cleaned = formula;
|
||||
cleaned.remove('`');
|
||||
cleaned.remove('\'');
|
||||
cleaned = cleaned.trimmed();
|
||||
if (cleaned.isEmpty())
|
||||
return QStringLiteral("empty");
|
||||
|
||||
// Parse with no callbacks — modules and dereferences succeed but return 0.
|
||||
// This checks syntax only.
|
||||
ExpressionParser parser(cleaned, nullptr);
|
||||
auto result = parser.parse();
|
||||
return result.ok ? QString() : result.error;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
27
src/addressparser.h
Normal file
27
src/addressparser.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct AddressParseResult {
|
||||
bool ok;
|
||||
uint64_t value;
|
||||
QString error;
|
||||
int errorPos;
|
||||
};
|
||||
|
||||
struct AddressParserCallbacks {
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
||||
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
|
||||
};
|
||||
|
||||
class AddressParser {
|
||||
public:
|
||||
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
|
||||
const AddressParserCallbacks* cb = nullptr);
|
||||
static QString validate(const QString& formula);
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
380
src/compose.cpp
380
src/compose.cpp
@@ -14,13 +14,16 @@ constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL;
|
||||
struct ComposeState {
|
||||
QString text;
|
||||
QVector<LineMeta> meta;
|
||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
||||
QSet<uint64_t> virtualPtrRefs; // refIds currently being virtually expanded via pointer deref
|
||||
int currentLine = 0;
|
||||
int typeW = kColType; // global type column width (fallback)
|
||||
int nameW = kColName; // global name column width (fallback)
|
||||
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
|
||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||
|
||||
// Precomputed for O(1) lookups
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
@@ -64,7 +67,6 @@ uint32_t computeMarkers(const Node& node, const Provider& /*prov*/,
|
||||
uint64_t /*addr*/, bool isCont, int /*depth*/) {
|
||||
uint32_t mask = 0;
|
||||
if (isCont) mask |= (1u << M_CONT);
|
||||
if (node.kind == NodeKind::Padding) mask |= (1u << M_PAD);
|
||||
// No ambient validation markers — errors only shown during inline editing.
|
||||
return mask;
|
||||
}
|
||||
@@ -77,12 +79,6 @@ static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
|
||||
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
|
||||
}
|
||||
|
||||
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
|
||||
if (tree.baseAddress == 0) return ptr;
|
||||
if (ptr >= tree.baseAddress) return ptr - tree.baseAddress;
|
||||
return UINT64_MAX; // Invalid: ptr below base address
|
||||
}
|
||||
|
||||
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
|
||||
int64_t total = 0;
|
||||
QSet<uint64_t> visited;
|
||||
@@ -109,6 +105,13 @@ static inline uint64_t resolveAddr(const ComposeState& state,
|
||||
return state.absOffsets[nodeIdx];
|
||||
}
|
||||
|
||||
|
||||
static const QVector<int>& childIndices(const ComposeState& state, uint64_t parentId) {
|
||||
static const QVector<int> kEmpty;
|
||||
auto it = state.childMap.constFind(parentId);
|
||||
return it == state.childMap.constEnd() ? kEmpty : it.value();
|
||||
}
|
||||
|
||||
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
const Provider& prov, int nodeIdx,
|
||||
int depth, uint64_t absAddr, uint64_t scopeId) {
|
||||
@@ -118,23 +121,30 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
int typeW = state.effectiveTypeW(scopeId);
|
||||
int nameW = state.effectiveNameW(scopeId);
|
||||
|
||||
// Line count: padding wraps at 8 bytes per line
|
||||
int numLines;
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
int totalBytes = qMax(1, node.arrayLen);
|
||||
numLines = (totalBytes + 7) / 8;
|
||||
} else {
|
||||
numLines = linesForKind(node.kind);
|
||||
}
|
||||
int numLines = linesForKind(node.kind);
|
||||
|
||||
// Resolve pointer target name for display
|
||||
QString ptrTypeOverride;
|
||||
QString ptrTargetName;
|
||||
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
|
||||
ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) {
|
||||
// Primitive pointer: e.g. "int32*" or "f64**"
|
||||
const auto* meta = kindMeta(node.elementKind);
|
||||
QString baseName = meta ? QString::fromLatin1(meta->typeName)
|
||||
: QStringLiteral("void");
|
||||
QString stars = (node.ptrDepth >= 2) ? QStringLiteral("**") : QStringLiteral("*");
|
||||
ptrTypeOverride = baseName + stars;
|
||||
} else {
|
||||
ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect type overflow in compact mode (for effectiveTypeW)
|
||||
QString rawType = ptrTypeOverride.isEmpty() ? fmt::typeNameRaw(node.kind) : ptrTypeOverride;
|
||||
bool typeOverflow = state.compactColumns && rawType.size() > typeW;
|
||||
int lineTypeW = typeOverflow ? rawType.size() : typeW;
|
||||
|
||||
for (int sub = 0; sub < numLines; sub++) {
|
||||
bool isCont = (sub > 0);
|
||||
|
||||
@@ -146,26 +156,23 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
lm.isContinuation = isCont;
|
||||
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveTypeW = lineTypeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
|
||||
// Set byte count for hex preview lines (used for per-byte change highlighting)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
int totalSz = qMax(1, node.arrayLen);
|
||||
lm.lineByteCount = qMin(8, totalSz - sub * 8);
|
||||
} else {
|
||||
lm.lineByteCount = sizeForKind(node.kind);
|
||||
}
|
||||
lm.lineByteCount = sizeForKind(node.kind);
|
||||
}
|
||||
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||
state.compactColumns);
|
||||
state.emitLine(lineText, lm);
|
||||
}
|
||||
}
|
||||
@@ -197,8 +204,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
@@ -215,8 +223,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::ArrayElementSeparator;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
@@ -244,16 +253,15 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = false;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = (1u << M_STRUCT_BG);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
|
||||
QString headerText;
|
||||
if (node.kind == NodeKind::Array) {
|
||||
@@ -264,19 +272,129 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.arrayCount = node.arrayLen;
|
||||
QString elemStructName = (node.elementKind == NodeKind::Struct)
|
||||
? resolvePointerTarget(tree, node.refId) : QString();
|
||||
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName);
|
||||
QString rawType = fmt::arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
||||
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName, state.compactColumns);
|
||||
} else {
|
||||
// All structs (root and nested) use the same header format
|
||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
|
||||
QString rawType = fmt::structTypeName(node);
|
||||
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
||||
}
|
||||
state.emitLine(headerText, lm);
|
||||
}
|
||||
|
||||
if (!node.collapsed || isArrayChild || isRootHeader) {
|
||||
QVector<int> children = state.childMap.value(node.id);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
// Enum with members: render name = value lines instead of offset-based fields
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||
int childDepth = depth + 1;
|
||||
int maxNameLen = 4;
|
||||
for (const auto& m : node.enumMembers)
|
||||
maxNameLen = qMax(maxNameLen, (int)m.first.size());
|
||||
|
||||
// Build display order sorted by value
|
||||
QVector<int> order(node.enumMembers.size());
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
std::sort(order.begin(), order.end(), [&](int a, int b) {
|
||||
return node.enumMembers[a].second < node.enumMembers[b].second;
|
||||
});
|
||||
|
||||
for (int oi = 0; oi < order.size(); oi++) {
|
||||
int mi = order[oi];
|
||||
const auto& m = node.enumMembers[mi];
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.subLine = mi;
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.isMemberLine = true;
|
||||
lm.nodeKind = NodeKind::UInt32;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.markerMask = 0;
|
||||
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);
|
||||
}
|
||||
|
||||
// Footer
|
||||
if (!isArrayChild) {
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Footer;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = isRootHeader;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bitfield with members: render name : width = value lines
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("bitfield")
|
||||
&& !node.bitfieldMembers.isEmpty()) {
|
||||
int childDepth = depth + 1;
|
||||
int maxNameLen = 4;
|
||||
for (const auto& m : node.bitfieldMembers)
|
||||
maxNameLen = qMax(maxNameLen, (int)m.name.size());
|
||||
|
||||
for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) {
|
||||
const auto& m = node.bitfieldMembers[mi];
|
||||
uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind,
|
||||
m.bitOffset, m.bitWidth);
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.subLine = mi;
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.isMemberLine = true;
|
||||
lm.nodeKind = node.elementKind;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.markerMask = 0;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
|
||||
childDepth, maxNameLen), lm);
|
||||
}
|
||||
|
||||
// Footer
|
||||
if (!isArrayChild) {
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Footer;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = isRootHeader;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
int sz = sizeForKind(node.elementKind);
|
||||
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.visiting.remove(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const QVector<int>& children = childIndices(state, node.id);
|
||||
|
||||
int childDepth = depth + 1;
|
||||
|
||||
@@ -307,15 +425,19 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.nodeKind = node.elementKind;
|
||||
lm.isArrayElement = true;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + elemAddr;
|
||||
lm.arrayElementIdx = i;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = elemAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.effectiveTypeW = eTW;
|
||||
bool elemOverflow = state.compactColumns && elemTypeStr.size() > eTW;
|
||||
lm.effectiveTypeW = elemOverflow ? elemTypeStr.size() : eTW;
|
||||
lm.effectiveNameW = eNW;
|
||||
|
||||
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
|
||||
{}, eTW, eNW, elemTypeStr), lm);
|
||||
{}, eTW, eNW, elemTypeStr,
|
||||
state.compactColumns), lm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,10 +463,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
QVector<int> refChildren = state.childMap.value(node.refId);
|
||||
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
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) {
|
||||
@@ -353,24 +472,27 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
if (state.visiting.contains(child.id)) {
|
||||
int typeW = state.effectiveTypeW(refScopeId);
|
||||
int nameW = state.effectiveNameW(refScopeId);
|
||||
QString rawType = fmt::structTypeName(child);
|
||||
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx; // parent struct — materialize target
|
||||
lm.nodeId = child.id;
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(
|
||||
tree.baseAddress + absAddr + child.offset, false,
|
||||
absAddr + child.offset, false,
|
||||
state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
|
||||
lm.offsetAddr = absAddr + child.offset;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = child.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = true;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, true);
|
||||
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
||||
/*collapsed=*/true, typeW, nameW), lm);
|
||||
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
|
||||
continue;
|
||||
}
|
||||
composeNode(state, tree, prov, childIdx, childDepth,
|
||||
@@ -404,8 +526,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
int sz = tree.structSpan(node.id, &state.childMap);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr + sz;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -430,29 +553,44 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
|
||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||
const QVector<int>& ptrChildren = childIndices(state, node.id);
|
||||
bool hasMaterialized = !ptrChildren.isEmpty();
|
||||
|
||||
// Force collapsed if this refId is already being virtually expanded
|
||||
// (prevents infinite recursion in virtual expansion mode).
|
||||
// Materialized children bypass this — they are real tree nodes with
|
||||
// independent collapsed state, so recursion is bounded by the tree.
|
||||
bool forceCollapsed = !hasMaterialized
|
||||
&& state.virtualPtrRefs.contains(node.refId);
|
||||
bool effectiveCollapsed = node.collapsed || forceCollapsed;
|
||||
|
||||
// Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
lm.foldCollapsed = effectiveCollapsed;
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
||||
lm.effectiveTypeW = typeW;
|
||||
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
||||
bool ptrOverflow = state.compactColumns && ptrTypeOverride.size() > typeW;
|
||||
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed,
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||
prov, absAddr, ptrTypeOverride,
|
||||
typeW, nameW), lm);
|
||||
typeW, nameW, state.compactColumns), lm);
|
||||
}
|
||||
|
||||
if (!node.collapsed) {
|
||||
if (!effectiveCollapsed) {
|
||||
int sz = node.byteSize();
|
||||
uint64_t ptrVal = 0;
|
||||
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
|
||||
@@ -462,38 +600,59 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
// Treat sentinel values as invalid pointers
|
||||
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
|
||||
ptrVal = 0;
|
||||
else {
|
||||
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
|
||||
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if pointer target is actually readable
|
||||
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
|
||||
// Pointer target address is used directly (absolute)
|
||||
uint64_t pBase = ptrVal;
|
||||
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
|
||||
|
||||
// For invalid/unreadable pointers: use NullProvider (shows zeros)
|
||||
// and reset margin offsets (unsigned wrap cancels baseAddress)
|
||||
static NullProvider s_nullProv;
|
||||
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
|
||||
if (!ptrReadable)
|
||||
pBase = (uint64_t)0 - tree.baseAddress;
|
||||
pBase = 0;
|
||||
|
||||
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||
if (!state.ptrVisiting.contains(key)) {
|
||||
state.ptrVisiting.insert(key);
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
depth, pBase, ref.id,
|
||||
/*isArrayChild=*/true);
|
||||
uint64_t savedPtrBase = state.currentPtrBase;
|
||||
state.currentPtrBase = pBase;
|
||||
|
||||
if (hasMaterialized) {
|
||||
// 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,
|
||||
pBase, node.id, false, node.id);
|
||||
}
|
||||
} else {
|
||||
// Virtual expansion via ref struct definition.
|
||||
// Temporarily remove the ref struct from visiting so composeParent
|
||||
// doesn't hit the struct-level cycle guard. The ptrVisiting mechanism
|
||||
// handles actual address-level pointer cycles, and virtualPtrRefs
|
||||
// prevents infinite virtual recursion (inner self-referential pointers
|
||||
// are force-collapsed with M_CYCLE for the user to materialize).
|
||||
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||
if (!state.ptrVisiting.contains(key)) {
|
||||
state.ptrVisiting.insert(key);
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
|
||||
bool wasVisiting = state.visiting.remove(node.refId);
|
||||
state.virtualPtrRefs.insert(node.refId);
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
depth, pBase, ref.id,
|
||||
/*isArrayChild=*/true);
|
||||
state.virtualPtrRefs.remove(node.refId);
|
||||
if (wasVisiting) state.visiting.insert(node.refId);
|
||||
}
|
||||
}
|
||||
state.ptrVisiting.remove(key);
|
||||
}
|
||||
state.ptrVisiting.remove(key);
|
||||
}
|
||||
|
||||
state.currentPtrBase = savedPtrBase;
|
||||
|
||||
// Footer for pointer fold
|
||||
{
|
||||
LineMeta lm;
|
||||
@@ -520,23 +679,32 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId) {
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||
bool compactColumns) {
|
||||
ComposeState state;
|
||||
state.compactColumns = compactColumns;
|
||||
|
||||
// Precompute parent→children map
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
state.childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
// Precompute absolute offsets
|
||||
for (auto it = state.childMap.begin(); it != state.childMap.end(); ++it) {
|
||||
QVector<int>& children = it.value();
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
}
|
||||
|
||||
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
||||
state.absOffsets.resize(tree.nodes.size());
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
state.absOffsets[i] = tree.computeOffset(i);
|
||||
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
|
||||
|
||||
// Compute hex digit tier from max absolute address
|
||||
{
|
||||
uint64_t maxAddr = tree.baseAddress;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
uint64_t addr = tree.baseAddress + (uint64_t)state.absOffsets[i];
|
||||
uint64_t addr = (uint64_t)state.absOffsets[i];
|
||||
if (addr > maxAddr) maxAddr = addr;
|
||||
}
|
||||
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;
|
||||
@@ -561,17 +729,18 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
|
||||
// Compute effective type column width from longest type name
|
||||
// Include struct/array headers which use "struct TypeName" or "type[count]" format
|
||||
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
|
||||
int maxTypeLen = kMinTypeW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
|
||||
}
|
||||
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
|
||||
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/padding (they show ASCII preview, not name column)
|
||||
// Skip hex (they show ASCII preview, not name column)
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||
}
|
||||
@@ -590,7 +759,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, but include containers)
|
||||
// Name width (skip hex, but include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
||||
}
|
||||
@@ -609,7 +778,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size());
|
||||
}
|
||||
|
||||
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW);
|
||||
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, typeCap);
|
||||
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
||||
}
|
||||
|
||||
@@ -622,17 +791,17 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, include containers)
|
||||
// Name width (skip hex, include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||
}
|
||||
}
|
||||
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW);
|
||||
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, typeCap);
|
||||
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
|
||||
}
|
||||
|
||||
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
|
||||
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE NoName {");
|
||||
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE 0x0 struct NoName {");
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = -1;
|
||||
@@ -643,16 +812,14 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
lm.foldHead = false;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = 0;
|
||||
lm.effectiveTypeW = state.typeW;
|
||||
lm.effectiveNameW = state.nameW;
|
||||
state.emitLine(cmdRowText, lm);
|
||||
}
|
||||
|
||||
QVector<int> roots = state.childMap.value(0);
|
||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
const QVector<int>& roots = childIndices(state, 0);
|
||||
|
||||
for (int idx : roots) {
|
||||
// If viewRootId is set, skip roots that don't match
|
||||
@@ -703,20 +870,5 @@ QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
|
||||
return result;
|
||||
}
|
||||
|
||||
int NodeTree::computeStructAlignment(uint64_t structId) const {
|
||||
int idx = indexOfId(structId);
|
||||
if (idx < 0) return 1;
|
||||
int maxAlign = 1;
|
||||
QVector<int> kids = childrenOf(structId);
|
||||
for (int ci : kids) {
|
||||
const Node& c = nodes[ci];
|
||||
if (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) {
|
||||
maxAlign = qMax(maxAlign, computeStructAlignment(c.id));
|
||||
} else {
|
||||
maxAlign = qMax(maxAlign, alignmentFor(c.kind));
|
||||
}
|
||||
}
|
||||
return maxAlign;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
1865
src/controller.cpp
1865
src/controller.cpp
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
#include <QUndoCommand>
|
||||
#include <QTimer>
|
||||
#include <QFutureWatcher>
|
||||
#include <QPointer>
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
@@ -39,7 +40,7 @@ public:
|
||||
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||
}
|
||||
|
||||
ComposeResult compose(uint64_t viewRootId = 0) const;
|
||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const;
|
||||
bool save(const QString& path);
|
||||
bool load(const QString& path);
|
||||
void loadData(const QString& binaryPath);
|
||||
@@ -69,6 +70,7 @@ struct SavedSourceEntry {
|
||||
QString filePath; // for File sources
|
||||
QString providerTarget; // for plugin providers (e.g. "pid:name")
|
||||
uint64_t baseAddress = 0;
|
||||
QString baseAddressFormula;
|
||||
};
|
||||
|
||||
// ── Controller ──
|
||||
@@ -84,20 +86,31 @@ public:
|
||||
void removeSplitEditor(RcxEditor* editor);
|
||||
QList<RcxEditor*> editors() const { return m_editors; }
|
||||
|
||||
void convertRootKeyword(const QString& newKeyword);
|
||||
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 removeNode(int nodeIdx);
|
||||
void toggleCollapse(int nodeIdx);
|
||||
void materializeRefChildren(int nodeIdx);
|
||||
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
|
||||
void setNodeValue(int nodeIdx, int subLine, const QString& text,
|
||||
bool isAscii = false, uint64_t resolvedAddr = 0);
|
||||
void duplicateNode(int nodeIdx);
|
||||
void convertToTypedPointer(uint64_t nodeId);
|
||||
void splitHexNode(uint64_t nodeId);
|
||||
void toggleBitfieldBit(uint64_t nodeId, int memberIdx);
|
||||
void editBitfieldValue(uint64_t nodeId, int memberIdx);
|
||||
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
||||
void batchRemoveNodes(const QVector<int>& nodeIndices);
|
||||
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
|
||||
void deleteRootStruct(uint64_t structId);
|
||||
void groupIntoUnion(const QSet<uint64_t>& nodeIds);
|
||||
void dissolveUnion(uint64_t unionId);
|
||||
|
||||
void applyCommand(const Command& cmd, bool isUndo);
|
||||
void refresh();
|
||||
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
|
||||
uint64_t findOrCreateStructByName(const QString& typeName);
|
||||
|
||||
// Selection
|
||||
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
|
||||
@@ -112,6 +125,8 @@ public:
|
||||
|
||||
RcxDocument* document() const { return m_doc; }
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
void setCompactColumns(bool v);
|
||||
|
||||
// MCP bridge accessors
|
||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||
@@ -119,6 +134,19 @@ public:
|
||||
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
|
||||
int activeSourceIndex() const { return m_activeSourceIdx; }
|
||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||
void clearSources();
|
||||
void selectSource(const QString& text);
|
||||
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
|
||||
|
||||
// Value tracking toggle (per-tab, off by default)
|
||||
bool trackValues() const { return m_trackValues; }
|
||||
void setTrackValues(bool on);
|
||||
|
||||
// Cross-tab type visibility: point at the project's full document list
|
||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||
|
||||
// Test accessor
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
|
||||
signals:
|
||||
void nodeSelected(int nodeIdx);
|
||||
@@ -131,6 +159,7 @@ private:
|
||||
QSet<uint64_t> m_selIds;
|
||||
int m_anchorLine = -1;
|
||||
bool m_suppressRefresh = false;
|
||||
bool m_compactColumns = false;
|
||||
uint64_t m_viewRootId = 0;
|
||||
|
||||
// ── Saved sources for quick-switch ──
|
||||
@@ -138,7 +167,7 @@ private:
|
||||
int m_activeSourceIdx = -1;
|
||||
|
||||
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
||||
TypeSelectorPopup* m_cachedPopup = nullptr;
|
||||
QPointer<TypeSelectorPopup> m_cachedPopup;
|
||||
|
||||
// ── Auto-refresh state ──
|
||||
using PageMap = QHash<uint64_t, QByteArray>;
|
||||
@@ -147,18 +176,20 @@ private:
|
||||
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||
PageMap m_prevPages;
|
||||
QSet<int64_t> m_changedOffsets;
|
||||
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||
bool m_trackValues = false;
|
||||
uint64_t m_refreshGen = 0;
|
||||
uint64_t m_readGen = 0;
|
||||
bool m_readInFlight = false;
|
||||
|
||||
QVector<RcxDocument*>* m_projectDocs = nullptr;
|
||||
|
||||
void connectEditor(RcxEditor* editor);
|
||||
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void updateCommandRow();
|
||||
void performRealignment(uint64_t structId, int targetAlign);
|
||||
void switchToSavedSource(int idx);
|
||||
void pushSavedSourcesToEditors();
|
||||
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
|
||||
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
|
||||
TypeSelectorPopup* ensurePopup(RcxEditor* editor);
|
||||
|
||||
// ── Auto-refresh methods ──
|
||||
@@ -169,7 +200,7 @@ private:
|
||||
void resetSnapshot();
|
||||
void collectPointerRanges(uint64_t structId, uint64_t memBase,
|
||||
int depth, int maxDepth,
|
||||
QSet<uint64_t>& visited,
|
||||
QSet<QPair<uint64_t,uint64_t>>& visited,
|
||||
QVector<QPair<uint64_t,int>>& ranges) const;
|
||||
};
|
||||
|
||||
|
||||
295
src/core.h
295
src/core.h
@@ -8,6 +8,7 @@
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
@@ -25,23 +26,23 @@ enum class NodeKind : uint8_t {
|
||||
UInt8, UInt16, UInt32, UInt64,
|
||||
Float, Double, Bool,
|
||||
Pointer32, Pointer64,
|
||||
FuncPtr32, FuncPtr64,
|
||||
Vec2, Vec3, Vec4, Mat4x4,
|
||||
UTF8, UTF16,
|
||||
Padding,
|
||||
Struct, Array
|
||||
};
|
||||
|
||||
} // namespace rcx (temporarily close for qHash)
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return ::qHash(static_cast<uint8_t>(key), seed); }
|
||||
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return qHash(static_cast<int>(key), seed); }
|
||||
#endif
|
||||
namespace rcx { // reopen
|
||||
|
||||
// ── Kind flags (replaces repeated Hex/Padding switches) ──
|
||||
// ── Kind flags (replaces repeated Hex switches) ──
|
||||
|
||||
enum KindFlags : uint32_t {
|
||||
KF_None = 0,
|
||||
KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout)
|
||||
KF_HexPreview = 1 << 0, // Hex8..Hex64 (ASCII+hex layout)
|
||||
KF_Container = 1 << 1, // Struct/Array
|
||||
KF_String = 1 << 2, // UTF8/UTF16
|
||||
KF_Vector = 1 << 3, // Vec2/3/4
|
||||
@@ -78,13 +79,14 @@ inline constexpr KindMeta kKindMeta[] = {
|
||||
{NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
|
||||
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
|
||||
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
|
||||
{NodeKind::FuncPtr32, "FuncPtr32", "fnptr32", 4, 1, 4, KF_None},
|
||||
{NodeKind::FuncPtr64, "FuncPtr64", "fnptr64", 8, 1, 8, KF_None},
|
||||
{NodeKind::Vec2, "Vec2", "vec2", 8, 1, 4, KF_Vector},
|
||||
{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::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
|
||||
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
|
||||
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
|
||||
};
|
||||
@@ -137,6 +139,18 @@ inline constexpr bool isVectorKind(NodeKind k) {
|
||||
inline constexpr bool isMatrixKind(NodeKind k) {
|
||||
return k == NodeKind::Mat4x4;
|
||||
}
|
||||
inline constexpr bool isFuncPtr(NodeKind k) {
|
||||
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
|
||||
}
|
||||
// Hex types, pointer types, function pointers, and containers are not meaningful
|
||||
// primitive-pointer targets — dereferencing them produces the same output as void*.
|
||||
inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
|
||||
if (isHexNode(k)) return false;
|
||||
if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false;
|
||||
if (isFuncPtr(k)) return false;
|
||||
if (k == NodeKind::Struct || k == NodeKind::Array) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
QStringList out;
|
||||
@@ -155,7 +169,6 @@ inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
|
||||
enum Marker : int {
|
||||
M_CONT = 0,
|
||||
M_PAD = 1,
|
||||
M_PTR0 = 2,
|
||||
M_CYCLE = 3,
|
||||
M_ERR = 4,
|
||||
@@ -166,6 +179,14 @@ enum Marker : int {
|
||||
M_ACCENT = 9,
|
||||
};
|
||||
|
||||
// ── Bitfield member (name + bit position + width within a container) ──
|
||||
|
||||
struct BitfieldMember {
|
||||
QString name;
|
||||
uint8_t bitOffset = 0; // position from LSB within the container
|
||||
uint8_t bitWidth = 1; // number of bits (1..64)
|
||||
};
|
||||
|
||||
// ── Node ──
|
||||
|
||||
struct Node {
|
||||
@@ -180,16 +201,28 @@ struct Node {
|
||||
int strLen = 64;
|
||||
bool collapsed = false;
|
||||
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type
|
||||
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**
|
||||
int viewIndex = 0; // Array: current view offset (transient)
|
||||
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
|
||||
QVector<BitfieldMember> bitfieldMembers; // Bitfield: per-bit member definitions
|
||||
|
||||
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
||||
int byteSize() const {
|
||||
switch (kind) {
|
||||
case NodeKind::UTF8: return strLen;
|
||||
case NodeKind::UTF16: return strLen * 2;
|
||||
case NodeKind::Padding: return qMax(1, arrayLen);
|
||||
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
|
||||
case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
|
||||
case NodeKind::Array: {
|
||||
int elemSz = sizeForKind(elementKind);
|
||||
if (elemSz <= 0) return 0;
|
||||
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
|
||||
}
|
||||
case NodeKind::Struct:
|
||||
if (classKeyword == QStringLiteral("bitfield")) {
|
||||
int sz = sizeForKind(elementKind);
|
||||
return sz > 0 ? sz : 4;
|
||||
}
|
||||
return 0;
|
||||
default: return sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
@@ -210,6 +243,29 @@ struct Node {
|
||||
o["collapsed"] = collapsed;
|
||||
o["refId"] = QString::number(refId);
|
||||
o["elementKind"] = kindToString(elementKind);
|
||||
if (ptrDepth > 0)
|
||||
o["ptrDepth"] = ptrDepth;
|
||||
if (!enumMembers.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const auto& m : enumMembers) {
|
||||
QJsonObject em;
|
||||
em["name"] = m.first;
|
||||
em["value"] = QString::number(m.second);
|
||||
arr.append(em);
|
||||
}
|
||||
o["enumMembers"] = arr;
|
||||
}
|
||||
if (!bitfieldMembers.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const auto& m : bitfieldMembers) {
|
||||
QJsonObject bm;
|
||||
bm["name"] = m.name;
|
||||
bm["bitOffset"] = m.bitOffset;
|
||||
bm["bitWidth"] = m.bitWidth;
|
||||
arr.append(bm);
|
||||
}
|
||||
o["bitfieldMembers"] = arr;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
static Node fromJson(const QJsonObject& o) {
|
||||
@@ -221,11 +277,31 @@ struct Node {
|
||||
n.classKeyword = o["classKeyword"].toString();
|
||||
n.parentId = o["parentId"].toString("0").toULongLong();
|
||||
n.offset = o["offset"].toInt(0);
|
||||
n.arrayLen = o["arrayLen"].toInt(1);
|
||||
n.strLen = o["strLen"].toInt(64);
|
||||
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.refId = o["refId"].toString("0").toULongLong();
|
||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||
if (o.contains("enumMembers")) {
|
||||
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()});
|
||||
}
|
||||
}
|
||||
if (o.contains("bitfieldMembers")) {
|
||||
QJsonArray arr = o["bitfieldMembers"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
QJsonObject bm = v.toObject();
|
||||
BitfieldMember m;
|
||||
m.name = bm["name"].toString();
|
||||
m.bitOffset = (uint8_t)bm["bitOffset"].toInt(0);
|
||||
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
|
||||
n.bitfieldMembers.append(m);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -247,6 +323,7 @@ struct Node {
|
||||
struct NodeTree {
|
||||
QVector<Node> nodes;
|
||||
uint64_t baseAddress = 0x00400000;
|
||||
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
||||
uint64_t m_nextId = 1;
|
||||
mutable QHash<uint64_t, int> m_idCache;
|
||||
|
||||
@@ -373,9 +450,6 @@ struct NodeTree {
|
||||
return qMax(declaredSize, maxEnd);
|
||||
}
|
||||
|
||||
// Compute natural alignment of a struct (max alignment of direct children)
|
||||
int computeStructAlignment(uint64_t structId) const;
|
||||
|
||||
// Batch selection normalizers
|
||||
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
|
||||
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
|
||||
@@ -383,6 +457,8 @@ struct NodeTree {
|
||||
QJsonObject toJson() const {
|
||||
QJsonObject o;
|
||||
o["baseAddress"] = QString::number(baseAddress, 16);
|
||||
if (!baseAddressFormula.isEmpty())
|
||||
o["baseAddressFormula"] = baseAddressFormula;
|
||||
o["nextId"] = QString::number(m_nextId);
|
||||
QJsonArray arr;
|
||||
for (const auto& n : nodes) arr.append(n.toJson());
|
||||
@@ -393,6 +469,7 @@ struct NodeTree {
|
||||
static NodeTree fromJson(const QJsonObject& o) {
|
||||
NodeTree t;
|
||||
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
||||
t.baseAddressFormula = o["baseAddressFormula"].toString();
|
||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||
QJsonArray arr = o["nodes"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
@@ -405,6 +482,49 @@ struct NodeTree {
|
||||
|
||||
};
|
||||
|
||||
// ── Value History (ring buffer for heatmap) ──
|
||||
|
||||
struct ValueHistory {
|
||||
static constexpr int kCapacity = 10;
|
||||
std::array<QString, kCapacity> values;
|
||||
int count = 0; // total unique values recorded
|
||||
int head = 0; // next write position in ring
|
||||
|
||||
void record(const QString& v) {
|
||||
if (count > 0) {
|
||||
int last = (head + kCapacity - 1) % kCapacity;
|
||||
if (values[last] == v) return; // no change
|
||||
}
|
||||
values[head] = v;
|
||||
head = (head + 1) % kCapacity;
|
||||
if (count < INT_MAX) count++;
|
||||
}
|
||||
|
||||
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||
|
||||
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||
int heatLevel() const {
|
||||
if (count <= 1) return 0;
|
||||
if (count == 2) return 1;
|
||||
if (count <= 4) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
QString last() const {
|
||||
if (count == 0) return {};
|
||||
return values[(head + kCapacity - 1) % kCapacity];
|
||||
}
|
||||
|
||||
// Iterate from oldest to newest (up to uniqueCount entries)
|
||||
template<typename Fn>
|
||||
void forEach(Fn&& fn) const {
|
||||
int n = uniqueCount();
|
||||
int start = (head + kCapacity - n) % kCapacity;
|
||||
for (int i = 0; i < n; i++)
|
||||
fn(values[(start + i) % kCapacity]);
|
||||
}
|
||||
};
|
||||
|
||||
// ── LineMeta ──
|
||||
|
||||
enum class LineKind : uint8_t {
|
||||
@@ -417,6 +537,29 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
|
||||
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
|
||||
|
||||
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
|
||||
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
||||
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
|
||||
}
|
||||
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
|
||||
return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift);
|
||||
}
|
||||
inline int memberSubFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
|
||||
}
|
||||
|
||||
struct LineMeta {
|
||||
int nodeIdx = -1;
|
||||
@@ -437,14 +580,17 @@ struct LineMeta {
|
||||
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
||||
QString offsetText;
|
||||
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
||||
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
|
||||
uint32_t markerMask = 0;
|
||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
|
||||
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
|
||||
int lineByteCount = 0; // Hex preview: actual data byte count on this line
|
||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
||||
bool isArrayElement = false; // true for synthesized primitive array element lines
|
||||
bool isMemberLine = false; // true for enum member / bitfield member lines
|
||||
};
|
||||
|
||||
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
@@ -479,7 +625,7 @@ namespace cmd {
|
||||
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
|
||||
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
||||
QVector<OffsetAdj> offAdjs; };
|
||||
struct ChangeBase { uint64_t oldBase, newBase; };
|
||||
struct ChangeBase { uint64_t oldBase, newBase; QString oldFormula, newFormula; };
|
||||
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
|
||||
struct ChangeArrayMeta { uint64_t nodeId;
|
||||
NodeKind oldElementKind, newElementKind;
|
||||
@@ -489,13 +635,15 @@ namespace cmd {
|
||||
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
|
||||
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
|
||||
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
|
||||
struct ChangeEnumMembers { uint64_t nodeId;
|
||||
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
|
||||
}
|
||||
|
||||
using Command = std::variant<
|
||||
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
|
||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers
|
||||
>;
|
||||
|
||||
// ── Column spans (for inline editing) ──
|
||||
@@ -522,20 +670,21 @@ inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uin
|
||||
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
|
||||
inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode
|
||||
|
||||
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
|
||||
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
||||
if (lm.lineKind != LineKind::Field || lm.isContinuation || lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
return {ind, ind + typeW, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
|
||||
if (lm.isContinuation || lm.lineKind != LineKind::Field || lm.isMemberLine) return {};
|
||||
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int start = ind + typeW + kSepWidth;
|
||||
|
||||
// Hex/Padding: ASCII preview occupies the name column (padded to nameW)
|
||||
// Hex: ASCII preview occupies the name column (padded to nameW)
|
||||
if (isHexPreview(lm.nodeKind))
|
||||
return {start, start + nameW, true};
|
||||
|
||||
@@ -545,11 +694,12 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
||||
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
|
||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||
if (lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
// Hex/Padding uses nameW for ASCII column (same as regular name column)
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
// Hex uses nameW for ASCII column (same as regular name column)
|
||||
bool isHex = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHex ? 23 : kColValue;
|
||||
|
||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||
|
||||
@@ -563,12 +713,33 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
return {start, start + valWidth, true};
|
||||
}
|
||||
|
||||
// Member line spans (enum "name = value", bitfield "name : N = value")
|
||||
inline ColumnSpan memberNameSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (!lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int eq = lineText.indexOf(QLatin1String(" = "), ind);
|
||||
if (eq < 0) return {};
|
||||
int nameEnd = eq;
|
||||
while (nameEnd > ind && lineText[nameEnd - 1] == ' ') nameEnd--;
|
||||
return {ind, nameEnd, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan memberValueSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (!lm.isMemberLine) return {};
|
||||
int eq = lineText.indexOf(QLatin1String(" = "));
|
||||
if (eq < 0) return {};
|
||||
int valStart = eq + 3;
|
||||
int valEnd = lineText.size();
|
||||
while (valEnd > valStart && lineText[valEnd - 1] == ' ') valEnd--;
|
||||
return {valStart, valEnd, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
bool isHex = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHex ? 23 : kColValue;
|
||||
|
||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||
int start;
|
||||
@@ -584,50 +755,56 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
// Line format: "source▾ · 0x140000000"
|
||||
|
||||
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (idx < 0) return {};
|
||||
// Source label ends at the ▾ dropdown arrow
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = 0;
|
||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
||||
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||
if (start >= idx) return {};
|
||||
// Exclude trailing ▾ from the editable span
|
||||
int end = idx;
|
||||
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (tag < 0) return {};
|
||||
int start = tag + 3; // after " · "
|
||||
int end = start;
|
||||
while (end < lineText.size() && !lineText[end].isSpace()) end++;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
if (start >= arrow) return {};
|
||||
return {start, arrow, true};
|
||||
}
|
||||
|
||||
// ── CommandRow root-class spans ──
|
||||
// Combined CommandRow format ends with: " struct▾ ClassName {"
|
||||
// Combined CommandRow format ends with: " struct ClassName {"
|
||||
|
||||
inline int commandRowRootStart(const QString& lineText) {
|
||||
int best = -1;
|
||||
int i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("struct\u25BE"));
|
||||
// Match "struct " / "class " / "enum " as whole words before the class name
|
||||
i = lineText.lastIndexOf(QStringLiteral("struct "));
|
||||
if (i > best) best = i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("class\u25BE"));
|
||||
i = lineText.lastIndexOf(QStringLiteral("class "));
|
||||
if (i > best) best = i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE"));
|
||||
i = lineText.lastIndexOf(QStringLiteral("enum "));
|
||||
if (i > best) best = i;
|
||||
return best;
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
// Address starts at "0x" after the source dropdown arrow
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = lineText.indexOf(QStringLiteral("0x"), arrow);
|
||||
if (start < 0) {
|
||||
// Formula mode: address is between arrow and root keyword
|
||||
start = arrow + 1;
|
||||
while (start < lineText.size() && lineText[start].isSpace()) start++;
|
||||
}
|
||||
// End at root keyword (struct/class/enum) or end of line
|
||||
int rootStart = commandRowRootStart(lineText);
|
||||
int end = (rootStart > start) ? rootStart : lineText.size();
|
||||
// Trim trailing whitespace
|
||||
while (end > start && lineText[end - 1].isSpace()) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
|
||||
int start = commandRowRootStart(lineText);
|
||||
if (start < 0) return {};
|
||||
int end = start;
|
||||
while (end < lineText.size() && lineText[end] != QChar(' ')
|
||||
&& lineText[end] != QChar(0x25BE)) end++;
|
||||
while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
@@ -771,17 +948,18 @@ namespace fmt {
|
||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
uint64_t addr, int depth, int subLine = 0,
|
||||
const QString& comment = {}, int colType = kColType, int colName = kColName,
|
||||
const QString& typeOverride = {});
|
||||
const QString& typeOverride = {}, bool compact = false);
|
||||
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName, bool compact = false);
|
||||
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
||||
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {});
|
||||
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {}, bool compact = false);
|
||||
QString structTypeName(const Node& node); // Full type string for struct headers
|
||||
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
|
||||
QString pointerTypeName(NodeKind kind, const QString& targetName);
|
||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||
const Provider& prov, uint64_t addr,
|
||||
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
|
||||
const QString& ptrTypeName, int colType = kColType, int colName = kColName,
|
||||
bool compact = false);
|
||||
QString validateBaseAddress(const QString& text);
|
||||
QString indent(int depth);
|
||||
QString readValue(const Node& node, const Provider& prov,
|
||||
@@ -791,10 +969,17 @@ namespace fmt {
|
||||
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
|
||||
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
||||
QString validateValue(NodeKind kind, const QString& text);
|
||||
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW);
|
||||
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
|
||||
uint64_t value, int depth, int nameW);
|
||||
uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
NodeKind containerKind,
|
||||
uint8_t bitOffset, uint8_t bitWidth);
|
||||
} // namespace fmt
|
||||
|
||||
// ── Compose function forward declaration ──
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||
bool compactColumns = false);
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
76
src/disasm.cpp
Normal file
76
src/disasm.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "disasm.h"
|
||||
|
||||
extern "C" {
|
||||
#include <fadec.h>
|
||||
}
|
||||
|
||||
namespace rcx {
|
||||
|
||||
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes) {
|
||||
if (bytes.isEmpty() || (bitness != 32 && bitness != 64))
|
||||
return {};
|
||||
|
||||
int len = qMin((int)bytes.size(), maxBytes);
|
||||
const auto* buf = reinterpret_cast<const uint8_t*>(bytes.constData());
|
||||
|
||||
QString result;
|
||||
int off = 0;
|
||||
while (off < len) {
|
||||
FdInstr instr;
|
||||
int ret = fd_decode(buf + off, len - off, bitness, baseAddr + off, &instr);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
char fmtBuf[128];
|
||||
fd_format(&instr, fmtBuf, sizeof(fmtBuf));
|
||||
|
||||
if (!result.isEmpty())
|
||||
result += QLatin1Char('\n');
|
||||
result += QStringLiteral("%1 %2")
|
||||
.arg(baseAddr + off, bitness == 64 ? 16 : 8, 16, QLatin1Char('0'))
|
||||
.arg(QString::fromLatin1(fmtBuf));
|
||||
|
||||
off += ret;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes) {
|
||||
if (bytes.isEmpty())
|
||||
return {};
|
||||
|
||||
int len = qMin((int)bytes.size(), maxBytes);
|
||||
QString result;
|
||||
|
||||
for (int off = 0; off < len; off += 16) {
|
||||
int lineLen = qMin(16, len - off);
|
||||
|
||||
if (!result.isEmpty())
|
||||
result += QLatin1Char('\n');
|
||||
|
||||
// Address
|
||||
bool wide = (baseAddr + len > 0xFFFFFFFFULL);
|
||||
result += QStringLiteral("%1 ").arg(baseAddr + off, wide ? 16 : 8, 16, QLatin1Char('0'));
|
||||
|
||||
// Hex bytes
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (i < lineLen) {
|
||||
uint8_t b = static_cast<uint8_t>(bytes[off + i]);
|
||||
result += QStringLiteral("%1 ").arg(b, 2, 16, QLatin1Char('0'));
|
||||
} else {
|
||||
result += QStringLiteral(" ");
|
||||
}
|
||||
if (i == 7) result += QLatin1Char(' ');
|
||||
}
|
||||
|
||||
// ASCII
|
||||
result += QLatin1Char(' ');
|
||||
for (int i = 0; i < lineLen; i++) {
|
||||
char c = bytes[off + i];
|
||||
result += (c >= 0x20 && c < 0x7f) ? QLatin1Char(c) : QLatin1Char('.');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
15
src/disasm.h
Normal file
15
src/disasm.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <cstdint>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Disassemble up to maxBytes of x86 code, returning formatted asm lines.
|
||||
// bitness: 32 or 64. Returns one line per instruction, prefixed with offset.
|
||||
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes = 128);
|
||||
|
||||
// Format bytes as hex dump lines (16 bytes per line with ASCII sidebar).
|
||||
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes = 128);
|
||||
|
||||
} // namespace rcx
|
||||
1081
src/editor.cpp
1081
src/editor.cpp
File diff suppressed because it is too large
Load Diff
31
src/editor.h
31
src/editor.h
@@ -4,6 +4,7 @@
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
@@ -27,6 +28,7 @@ public:
|
||||
void restoreViewState(const ViewState& vs);
|
||||
|
||||
QsciScintilla* scintilla() const { return m_sci; }
|
||||
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
|
||||
const LineMeta* metaForLine(int line) const;
|
||||
int currentNodeIndex() const;
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
@@ -54,6 +56,12 @@ public:
|
||||
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
||||
QString textWithMargins() const;
|
||||
void setCustomTypeNames(const QStringList& names);
|
||||
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
|
||||
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
|
||||
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
|
||||
}
|
||||
|
||||
void setRelativeOffsets(bool rel) { m_relativeOffsets = rel; reformatMargins(); }
|
||||
|
||||
// Saved sources for quick-switch in source picker
|
||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||
@@ -61,9 +69,11 @@ public:
|
||||
signals:
|
||||
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
|
||||
void keywordConvertRequested(const QString& newKeyword);
|
||||
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
|
||||
void inlineEditCommitted(int nodeIdx, int subLine,
|
||||
EditTarget target, const QString& text);
|
||||
EditTarget target, const QString& text,
|
||||
uint64_t resolvedAddr = 0);
|
||||
void inlineEditCancelled();
|
||||
void typeSelectorRequested();
|
||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||
@@ -78,7 +88,7 @@ private:
|
||||
LayoutInfo m_layout; // cached from ComposeResult
|
||||
|
||||
// ── Toggle: absolute vs relative offset margin
|
||||
bool m_relativeOffsets = false;
|
||||
bool m_relativeOffsets = true;
|
||||
|
||||
int m_marginStyleBase = -1;
|
||||
int m_hintLine = -1;
|
||||
@@ -88,8 +98,12 @@ private:
|
||||
bool m_hoverInside = false;
|
||||
uint64_t m_hoveredNodeId = 0;
|
||||
int m_hoveredLine = -1;
|
||||
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
|
||||
int m_prevHoveredLine = -1; // for incremental marker update
|
||||
QSet<uint64_t> m_currentSelIds;
|
||||
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
|
||||
// ── nodeId → display-line index (built in applyDocument) ──
|
||||
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
|
||||
// ── Drag selection ──
|
||||
bool m_dragging = false;
|
||||
bool m_dragStarted = false; // true once drag threshold exceeded
|
||||
@@ -129,7 +143,17 @@ private:
|
||||
// ── Saved sources for quick-switch ──
|
||||
QVector<SavedSourceDisplay> m_savedSourceDisplay;
|
||||
|
||||
// ── 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)
|
||||
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;
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
bool m_updatingComment = false;
|
||||
|
||||
@@ -145,7 +169,8 @@ private:
|
||||
void applyMarkers(const QVector<LineMeta>& meta);
|
||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta);
|
||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||
void applyCommandRowPills();
|
||||
|
||||
|
||||
356
src/examples/EPROCESS.rcx
Normal file
356
src/examples/EPROCESS.rcx
Normal file
@@ -0,0 +1,356 @@
|
||||
{
|
||||
"baseAddress": "FFFF800000000000",
|
||||
"nextId": "9000",
|
||||
"nodes": [
|
||||
{"id":"100","kind":"Struct","name":"list_entry","structTypeName":"_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"101","kind":"Pointer64","name":"Flink","offset":0,"parentId":"100","refId":"100","collapsed":true},
|
||||
{"id":"102","kind":"Pointer64","name":"Blink","offset":8,"parentId":"100","refId":"100","collapsed":true},
|
||||
|
||||
{"id":"110","kind":"Struct","name":"single_list_entry","structTypeName":"_SINGLE_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"111","kind":"Pointer64","name":"Next","offset":0,"parentId":"110","refId":"110","collapsed":true},
|
||||
|
||||
{"id":"120","kind":"Struct","name":"ex_push_lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"121","kind":"Hex64","name":"Value","offset":0,"parentId":"120"},
|
||||
|
||||
{"id":"130","kind":"Struct","name":"ex_rundown_ref","structTypeName":"_EX_RUNDOWN_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"131","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"130","refId":"0","collapsed":false},
|
||||
{"id":"132","kind":"UInt64","name":"Count","offset":0,"parentId":"131"},
|
||||
{"id":"133","kind":"Pointer64","name":"Ptr","offset":0,"parentId":"131"},
|
||||
|
||||
{"id":"140","kind":"Struct","name":"ex_fast_ref","structTypeName":"_EX_FAST_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"141","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"140","refId":"0","collapsed":false},
|
||||
{"id":"142","kind":"Pointer64","name":"Object","offset":0,"parentId":"141"},
|
||||
{"id":"143","kind":"UInt64","name":"Value","offset":0,"parentId":"141"},
|
||||
|
||||
{"id":"150","kind":"Struct","name":"unicode_string","structTypeName":"_UNICODE_STRING","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"151","kind":"UInt16","name":"Length","offset":0,"parentId":"150"},
|
||||
{"id":"152","kind":"UInt16","name":"MaximumLength","offset":2,"parentId":"150"},
|
||||
{"id":"153","kind":"Pointer64","name":"Buffer","offset":8,"parentId":"150"},
|
||||
|
||||
{"id":"160","kind":"Struct","name":"large_integer","structTypeName":"_LARGE_INTEGER","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"161","kind":"Struct","name":"","offset":0,"parentId":"160","refId":"0","collapsed":false},
|
||||
{"id":"162","kind":"UInt32","name":"LowPart","offset":0,"parentId":"161"},
|
||||
{"id":"163","kind":"Int32","name":"HighPart","offset":4,"parentId":"161"},
|
||||
{"id":"164","kind":"Int64","name":"QuadPart","offset":0,"parentId":"160"},
|
||||
|
||||
{"id":"170","kind":"Struct","name":"rtl_avl_tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"171","kind":"Pointer64","name":"Root","offset":0,"parentId":"170"},
|
||||
|
||||
{"id":"180","kind":"Struct","name":"kstack_count","structTypeName":"_KSTACK_COUNT","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"181","kind":"Int32","name":"Value","offset":0,"parentId":"180"},
|
||||
{"id":"182","kind":"Hex32","name":"State:3 StackCount:29","offset":0,"parentId":"180"},
|
||||
|
||||
{"id":"190","kind":"Struct","name":"kexecute_options","structTypeName":"_KEXECUTE_OPTIONS","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"191","kind":"Struct","name":"","offset":0,"parentId":"190","refId":"0","collapsed":false},
|
||||
{"id":"192","kind":"UInt8","name":"ExecuteDisable","offset":0,"parentId":"191"},
|
||||
{"id":"193","kind":"Hex8","name":"ExecuteDisable:1 ExecuteEnable:1 DisableThunkEmulation:1 Permanent:1 ExecuteDispatchEnable:1 ImageDispatchEnable:1 DisableExceptionChainValidation:1 Spare:1","offset":0,"parentId":"191"},
|
||||
{"id":"194","kind":"UInt8","name":"ExecuteOptions","offset":0,"parentId":"190"},
|
||||
|
||||
{"id":"200","kind":"Struct","name":"se_audit_info","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"201","kind":"Pointer64","name":"ImageFileName","offset":0,"parentId":"200"},
|
||||
|
||||
{"id":"210","kind":"Struct","name":"ps_protection","structTypeName":"_PS_PROTECTION","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"211","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"210","refId":"0","collapsed":false},
|
||||
{"id":"212","kind":"UInt8","name":"Level","offset":0,"parentId":"211"},
|
||||
{"id":"213","kind":"Hex8","name":"Type:3 Audit:1 Signer:4","offset":0,"parentId":"211"},
|
||||
|
||||
{"id":"220","kind":"Struct","name":"timer_delay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"221","kind":"Hex64","name":"DelayMs:30 CoalescingWindowMs:30 Reserved:1 NewTimerWheel:1 Retry:1 Locked:1","offset":0,"parentId":"220"},
|
||||
{"id":"222","kind":"UInt64","name":"All","offset":0,"parentId":"220"},
|
||||
|
||||
{"id":"230","kind":"Struct","name":"wnf_state_name","structTypeName":"_WNF_STATE_NAME","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"231","kind":"UInt32","name":"Data_0","offset":0,"parentId":"230"},
|
||||
{"id":"232","kind":"UInt32","name":"Data_1","offset":4,"parentId":"230"},
|
||||
|
||||
{"id":"240","kind":"Struct","name":"dynamic_ranges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"241","kind":"Struct","name":"Tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"240","refId":"170","collapsed":true},
|
||||
{"id":"242","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":8,"parentId":"240","refId":"120","collapsed":true},
|
||||
|
||||
{"id":"250","kind":"Struct","name":"alpc_context","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"251","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"250","refId":"120","collapsed":true},
|
||||
{"id":"252","kind":"Struct","name":"ViewListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"250","refId":"100","collapsed":true},
|
||||
{"id":"253","kind":"UInt64","name":"PagedPoolQuotaCache","offset":24,"parentId":"250"},
|
||||
|
||||
{"id":"260","kind":"Struct","name":"mmsupport_flags","structTypeName":"_MMSUPPORT_FLAGS","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"261","kind":"Hex32","name":"EntireFlags","offset":0,"parentId":"260"},
|
||||
|
||||
{"id":"270","kind":"Struct","name":"mmsupport_shared","structTypeName":"_MMSUPPORT_SHARED","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"271","kind":"Pointer64","name":"WorkingSetLockArray","offset":0,"parentId":"270"},
|
||||
{"id":"272","kind":"UInt64","name":"ReleasedCommitDebt","offset":8,"parentId":"270"},
|
||||
{"id":"273","kind":"UInt64","name":"ResetPagesRepurposedCount","offset":16,"parentId":"270"},
|
||||
{"id":"274","kind":"Pointer64","name":"WsSwapSupport","offset":24,"parentId":"270"},
|
||||
{"id":"275","kind":"Pointer64","name":"CommitReleaseContext","offset":32,"parentId":"270"},
|
||||
{"id":"276","kind":"Pointer64","name":"AccessLog","offset":40,"parentId":"270"},
|
||||
{"id":"277","kind":"UInt64","name":"ChargedWslePages","offset":48,"parentId":"270"},
|
||||
{"id":"278","kind":"UInt64","name":"ActualWslePages","offset":56,"parentId":"270"},
|
||||
{"id":"279","kind":"Int32","name":"WorkingSetCoreLock","offset":64,"parentId":"270"},
|
||||
{"id":"280","kind":"Pointer64","name":"ShadowMapping","offset":72,"parentId":"270"},
|
||||
|
||||
{"id":"300","kind":"Struct","name":"mmsupport_instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"301","kind":"UInt32","name":"NextPageColor","offset":0,"parentId":"300"},
|
||||
{"id":"302","kind":"UInt32","name":"PageFaultCount","offset":4,"parentId":"300"},
|
||||
{"id":"303","kind":"UInt64","name":"TrimmedPageCount","offset":8,"parentId":"300"},
|
||||
{"id":"304","kind":"Pointer64","name":"VmWorkingSetList","offset":16,"parentId":"300"},
|
||||
{"id":"305","kind":"Struct","name":"WorkingSetExpansionLinks","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"300","refId":"100","collapsed":true},
|
||||
{"id":"306","kind":"UInt64","name":"AgeDistribution_0","offset":40,"parentId":"300"},
|
||||
{"id":"307","kind":"UInt64","name":"AgeDistribution_1","offset":48,"parentId":"300"},
|
||||
{"id":"308","kind":"UInt64","name":"AgeDistribution_2","offset":56,"parentId":"300"},
|
||||
{"id":"309","kind":"UInt64","name":"AgeDistribution_3","offset":64,"parentId":"300"},
|
||||
{"id":"310","kind":"UInt64","name":"AgeDistribution_4","offset":72,"parentId":"300"},
|
||||
{"id":"311","kind":"UInt64","name":"AgeDistribution_5","offset":80,"parentId":"300"},
|
||||
{"id":"312","kind":"UInt64","name":"AgeDistribution_6","offset":88,"parentId":"300"},
|
||||
{"id":"313","kind":"UInt64","name":"AgeDistribution_7","offset":96,"parentId":"300"},
|
||||
{"id":"314","kind":"Pointer64","name":"ExitOutswapGate","offset":104,"parentId":"300"},
|
||||
{"id":"315","kind":"UInt64","name":"MinimumWorkingSetSize","offset":112,"parentId":"300"},
|
||||
{"id":"316","kind":"UInt64","name":"MaximumWorkingSetSize","offset":120,"parentId":"300"},
|
||||
{"id":"317","kind":"UInt64","name":"WorkingSetLeafSize","offset":128,"parentId":"300"},
|
||||
{"id":"318","kind":"UInt64","name":"WorkingSetLeafPrivateSize","offset":136,"parentId":"300"},
|
||||
{"id":"319","kind":"UInt64","name":"WorkingSetSize","offset":144,"parentId":"300"},
|
||||
{"id":"320","kind":"UInt64","name":"WorkingSetPrivateSize","offset":152,"parentId":"300"},
|
||||
{"id":"321","kind":"UInt64","name":"PeakWorkingSetSize","offset":160,"parentId":"300"},
|
||||
{"id":"322","kind":"UInt32","name":"HardFaultCount","offset":168,"parentId":"300"},
|
||||
{"id":"323","kind":"UInt16","name":"LastTrimStamp","offset":172,"parentId":"300"},
|
||||
{"id":"324","kind":"UInt16","name":"PartitionId","offset":174,"parentId":"300"},
|
||||
{"id":"325","kind":"UInt64","name":"SelfmapLock","offset":176,"parentId":"300"},
|
||||
{"id":"326","kind":"Struct","name":"Flags","structTypeName":"_MMSUPPORT_FLAGS","offset":184,"parentId":"300","refId":"260","collapsed":true},
|
||||
|
||||
{"id":"350","kind":"Struct","name":"mmsupport_full","structTypeName":"_MMSUPPORT_FULL","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"351","kind":"Struct","name":"Instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"350","refId":"300","collapsed":true},
|
||||
{"id":"352","kind":"Struct","name":"Shared","structTypeName":"_MMSUPPORT_SHARED","offset":192,"parentId":"350","refId":"270","collapsed":true},
|
||||
|
||||
{"id":"400","kind":"Struct","name":"dispatcher_header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"401","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"400","refId":"0","collapsed":false},
|
||||
{"id":"402","kind":"UInt8","name":"Type","offset":0,"parentId":"401"},
|
||||
{"id":"403","kind":"UInt8","name":"Signalling","offset":1,"parentId":"401"},
|
||||
{"id":"404","kind":"UInt8","name":"Size","offset":2,"parentId":"401"},
|
||||
{"id":"405","kind":"UInt8","name":"Reserved1","offset":3,"parentId":"401"},
|
||||
{"id":"406","kind":"Int32","name":"Lock","offset":0,"parentId":"401"},
|
||||
{"id":"407","kind":"Int32","name":"SignalState","offset":4,"parentId":"400"},
|
||||
{"id":"408","kind":"Struct","name":"WaitListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"400","refId":"100","collapsed":true},
|
||||
|
||||
{"id":"500","kind":"Struct","name":"kprocess","structTypeName":"_KPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"501","kind":"Struct","name":"Header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"500","refId":"400","collapsed":true},
|
||||
{"id":"502","kind":"Struct","name":"ProfileListHead","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"503","kind":"UInt64","name":"DirectoryTableBase","offset":40,"parentId":"500"},
|
||||
{"id":"504","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":48,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"505","kind":"UInt32","name":"ProcessLock","offset":64,"parentId":"500"},
|
||||
{"id":"506","kind":"UInt32","name":"ProcessTimerDelay","offset":68,"parentId":"500"},
|
||||
{"id":"507","kind":"UInt64","name":"DeepFreezeStartTime","offset":72,"parentId":"500"},
|
||||
{"id":"508","kind":"Pointer64","name":"Affinity","offset":80,"parentId":"500"},
|
||||
{"id":"509","kind":"Hex64","name":"AutoBoostState","offset":88,"parentId":"500"},
|
||||
{"id":"510","kind":"Struct","name":"ReadyListHead","structTypeName":"_LIST_ENTRY","offset":104,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"511","kind":"Struct","name":"SwapListEntry","structTypeName":"_SINGLE_LIST_ENTRY","offset":120,"parentId":"500","refId":"110","collapsed":true},
|
||||
{"id":"512","kind":"Pointer64","name":"ActiveProcessors","offset":128,"parentId":"500"},
|
||||
{"id":"513","kind":"Struct","name":"","classKeyword":"union","offset":136,"parentId":"500","refId":"0","collapsed":false},
|
||||
{"id":"514","kind":"Hex32","name":"AutoAlignment:1 DisableBoost:1 DisableQuantum:1 DeepFreeze:1 TimerVirtualization:1 CheckStackExtents:1 CacheIsolationEnabled:1 PpmPolicy:4 VaSpaceDeleted:1 MultiGroup:1 ForegroundProcess:1 ReservedFlags:18","offset":0,"parentId":"513"},
|
||||
{"id":"515","kind":"Int32","name":"ProcessFlags","offset":0,"parentId":"513"},
|
||||
{"id":"516","kind":"Int8","name":"BasePriority","offset":144,"parentId":"500"},
|
||||
{"id":"517","kind":"Int8","name":"QuantumReset","offset":145,"parentId":"500"},
|
||||
{"id":"518","kind":"Int8","name":"Visited","offset":146,"parentId":"500"},
|
||||
{"id":"519","kind":"Struct","name":"Flags","structTypeName":"_KEXECUTE_OPTIONS","offset":147,"parentId":"500","refId":"190","collapsed":true},
|
||||
{"id":"520","kind":"Struct","name":"StackCount","structTypeName":"_KSTACK_COUNT","offset":264,"parentId":"500","refId":"180","collapsed":true},
|
||||
{"id":"521","kind":"Struct","name":"ProcessListEntry","structTypeName":"_LIST_ENTRY","offset":272,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"522","kind":"UInt64","name":"CycleTime","offset":288,"parentId":"500"},
|
||||
{"id":"523","kind":"UInt64","name":"ContextSwitches","offset":296,"parentId":"500"},
|
||||
{"id":"524","kind":"Pointer64","name":"SchedulingGroup","offset":304,"parentId":"500"},
|
||||
{"id":"525","kind":"UInt64","name":"KernelTime","offset":312,"parentId":"500"},
|
||||
{"id":"526","kind":"UInt64","name":"UserTime","offset":320,"parentId":"500"},
|
||||
{"id":"527","kind":"UInt64","name":"ReadyTime","offset":328,"parentId":"500"},
|
||||
{"id":"528","kind":"UInt32","name":"FreezeCount","offset":336,"parentId":"500"},
|
||||
{"id":"529","kind":"UInt64","name":"UserDirectoryTableBase","offset":344,"parentId":"500"},
|
||||
{"id":"530","kind":"UInt8","name":"AddressPolicy","offset":352,"parentId":"500"},
|
||||
{"id":"531","kind":"Pointer64","name":"InstrumentationCallback","offset":360,"parentId":"500"},
|
||||
{"id":"532","kind":"UInt64","name":"SecureHandle","offset":368,"parentId":"500"},
|
||||
{"id":"533","kind":"UInt64","name":"KernelWaitTime","offset":376,"parentId":"500"},
|
||||
{"id":"534","kind":"UInt64","name":"UserWaitTime","offset":384,"parentId":"500"},
|
||||
{"id":"535","kind":"UInt64","name":"LastRebalanceQpc","offset":392,"parentId":"500"},
|
||||
{"id":"536","kind":"Pointer64","name":"PerProcessorCycleTimes","offset":400,"parentId":"500"},
|
||||
{"id":"537","kind":"UInt64","name":"ExtendedFeatureDisableMask","offset":408,"parentId":"500"},
|
||||
{"id":"538","kind":"UInt16","name":"PrimaryGroup","offset":416,"parentId":"500"},
|
||||
{"id":"539","kind":"Pointer64","name":"UserCetLogging","offset":424,"parentId":"500"},
|
||||
{"id":"540","kind":"Struct","name":"CpuPartitionList","structTypeName":"_LIST_ENTRY","offset":432,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"541","kind":"Pointer64","name":"AvailableCpuState","offset":448,"parentId":"500"},
|
||||
|
||||
{"id":"2000","kind":"Struct","name":"eprocess","structTypeName":"_EPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":false},
|
||||
{"id":"2001","kind":"Struct","name":"Pcb","structTypeName":"_KPROCESS","offset":0,"parentId":"2000","refId":"500","collapsed":true},
|
||||
{"id":"2002","kind":"Struct","name":"ProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":456,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2003","kind":"Pointer64","name":"UniqueProcessId","offset":464,"parentId":"2000"},
|
||||
{"id":"2004","kind":"Struct","name":"ActiveProcessLinks","structTypeName":"_LIST_ENTRY","offset":472,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2005","kind":"Struct","name":"RundownProtect","structTypeName":"_EX_RUNDOWN_REF","offset":488,"parentId":"2000","refId":"130","collapsed":true},
|
||||
{"id":"2006","kind":"Struct","name":"","classKeyword":"union","offset":496,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2007","kind":"UInt32","name":"Flags2","offset":0,"parentId":"2006"},
|
||||
{"id":"2008","kind":"Hex32","name":"JobNotReallyActive:1 AccountingFolded:1 NewProcessReported:1 ExitProcessReported:1 ReportCommitChanges:1 LastReportMemory:1 ForceWakeCharge:1 CrossSessionCreate:1 NeedsHandleRundown:1 RefTraceEnabled:1 PicoCreated:1 EmptyJobEvaluated:1 DefaultPagePriority:3 PrimaryTokenFrozen:1","offset":0,"parentId":"2006"},
|
||||
{"id":"2009","kind":"Struct","name":"","classKeyword":"union","offset":500,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2010","kind":"UInt32","name":"Flags","offset":0,"parentId":"2009"},
|
||||
{"id":"2011","kind":"Hex32","name":"CreateReported:1 NoDebugInherit:1 ProcessExiting:1 ProcessDelete:1 ManageExecutableMemoryWrites:1 VmDeleted:1 OutswapEnabled:1 Outswapped:1 FailFastOnCommitFail:1 Wow64VaSpace4Gb:1 AddressSpaceInitialized:2 SetTimerResolution:1 BreakOnTermination:1","offset":0,"parentId":"2009"},
|
||||
{"id":"2012","kind":"Int64","name":"CreateTime","offset":504,"parentId":"2000"},
|
||||
{"id":"2013","kind":"UInt64","name":"ProcessQuotaUsage_0","offset":512,"parentId":"2000"},
|
||||
{"id":"2014","kind":"UInt64","name":"ProcessQuotaUsage_1","offset":520,"parentId":"2000"},
|
||||
{"id":"2015","kind":"UInt64","name":"ProcessQuotaPeak_0","offset":528,"parentId":"2000"},
|
||||
{"id":"2016","kind":"UInt64","name":"ProcessQuotaPeak_1","offset":536,"parentId":"2000"},
|
||||
{"id":"2017","kind":"UInt64","name":"PeakVirtualSize","offset":544,"parentId":"2000"},
|
||||
{"id":"2018","kind":"UInt64","name":"VirtualSize","offset":552,"parentId":"2000"},
|
||||
{"id":"2019","kind":"Struct","name":"SessionProcessLinks","structTypeName":"_LIST_ENTRY","offset":560,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2020","kind":"Struct","name":"","classKeyword":"union","offset":576,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2021","kind":"Pointer64","name":"ExceptionPortData","offset":0,"parentId":"2020"},
|
||||
{"id":"2022","kind":"UInt64","name":"ExceptionPortValue","offset":0,"parentId":"2020"},
|
||||
{"id":"2023","kind":"Struct","name":"Token","structTypeName":"_EX_FAST_REF","offset":584,"parentId":"2000","refId":"140","collapsed":true},
|
||||
{"id":"2024","kind":"UInt64","name":"MmReserved","offset":592,"parentId":"2000"},
|
||||
{"id":"2025","kind":"Struct","name":"AddressCreationLock","structTypeName":"_EX_PUSH_LOCK","offset":600,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2026","kind":"Struct","name":"PageTableCommitmentLock","structTypeName":"_EX_PUSH_LOCK","offset":608,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2027","kind":"Pointer64","name":"RotateInProgress","offset":616,"parentId":"2000"},
|
||||
{"id":"2028","kind":"Pointer64","name":"ForkInProgress","offset":624,"parentId":"2000"},
|
||||
{"id":"2029","kind":"Pointer64","name":"CommitChargeJob","offset":632,"parentId":"2000"},
|
||||
{"id":"2030","kind":"Struct","name":"CloneRoot","structTypeName":"_RTL_AVL_TREE","offset":640,"parentId":"2000","refId":"170","collapsed":true},
|
||||
{"id":"2031","kind":"UInt64","name":"NumberOfPrivatePages","offset":648,"parentId":"2000"},
|
||||
{"id":"2032","kind":"UInt64","name":"NumberOfLockedPages","offset":656,"parentId":"2000"},
|
||||
{"id":"2033","kind":"Pointer64","name":"Win32Process","offset":664,"parentId":"2000"},
|
||||
{"id":"2034","kind":"Pointer64","name":"Job","offset":672,"parentId":"2000"},
|
||||
{"id":"2035","kind":"Pointer64","name":"SectionObject","offset":680,"parentId":"2000"},
|
||||
{"id":"2036","kind":"Pointer64","name":"SectionBaseAddress","offset":688,"parentId":"2000"},
|
||||
{"id":"2037","kind":"UInt32","name":"Cookie","offset":696,"parentId":"2000"},
|
||||
{"id":"2038","kind":"Pointer64","name":"WorkingSetWatch","offset":704,"parentId":"2000"},
|
||||
{"id":"2039","kind":"Pointer64","name":"Win32WindowStation","offset":712,"parentId":"2000"},
|
||||
{"id":"2040","kind":"Pointer64","name":"InheritedFromUniqueProcessId","offset":720,"parentId":"2000"},
|
||||
{"id":"2041","kind":"UInt64","name":"OwnerProcessId","offset":728,"parentId":"2000"},
|
||||
{"id":"2042","kind":"Pointer64","name":"Peb","offset":736,"parentId":"2000"},
|
||||
{"id":"2043","kind":"Pointer64","name":"Session","offset":744,"parentId":"2000"},
|
||||
{"id":"2044","kind":"Pointer64","name":"Spare1","offset":752,"parentId":"2000"},
|
||||
{"id":"2045","kind":"Pointer64","name":"QuotaBlock","offset":760,"parentId":"2000"},
|
||||
{"id":"2046","kind":"Pointer64","name":"ObjectTable","offset":768,"parentId":"2000"},
|
||||
{"id":"2047","kind":"Pointer64","name":"DebugPort","offset":776,"parentId":"2000"},
|
||||
{"id":"2048","kind":"Pointer64","name":"WoW64Process","offset":784,"parentId":"2000"},
|
||||
{"id":"2049","kind":"Struct","name":"DeviceMap","structTypeName":"_EX_FAST_REF","offset":792,"parentId":"2000","refId":"140","collapsed":true},
|
||||
{"id":"2050","kind":"Pointer64","name":"EtwDataSource","offset":800,"parentId":"2000"},
|
||||
{"id":"2051","kind":"UInt64","name":"PageDirectoryPte","offset":808,"parentId":"2000"},
|
||||
{"id":"2052","kind":"Pointer64","name":"ImageFilePointer","offset":816,"parentId":"2000"},
|
||||
{"id":"2053","kind":"Hex64","name":"ImageFileName_lo","offset":824,"parentId":"2000"},
|
||||
{"id":"2054","kind":"Hex32","name":"ImageFileName_mi","offset":832,"parentId":"2000"},
|
||||
{"id":"2055","kind":"Hex16","name":"ImageFileName_hi","offset":836,"parentId":"2000"},
|
||||
{"id":"2056","kind":"UInt8","name":"ImageFileName_14","offset":838,"parentId":"2000"},
|
||||
{"id":"2057","kind":"UInt8","name":"PriorityClass","offset":839,"parentId":"2000"},
|
||||
{"id":"2058","kind":"Pointer64","name":"SecurityPort","offset":840,"parentId":"2000"},
|
||||
{"id":"2059","kind":"Struct","name":"SeAuditProcessCreationInfo","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":848,"parentId":"2000","refId":"200","collapsed":true},
|
||||
{"id":"2060","kind":"Struct","name":"JobLinks","structTypeName":"_LIST_ENTRY","offset":856,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2061","kind":"Pointer64","name":"HighestUserAddress","offset":872,"parentId":"2000"},
|
||||
{"id":"2062","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":880,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2063","kind":"UInt32","name":"ActiveThreads","offset":896,"parentId":"2000"},
|
||||
{"id":"2064","kind":"UInt32","name":"ImagePathHash","offset":900,"parentId":"2000"},
|
||||
{"id":"2065","kind":"UInt32","name":"DefaultHardErrorProcessing","offset":904,"parentId":"2000"},
|
||||
{"id":"2066","kind":"Int32","name":"LastThreadExitStatus","offset":908,"parentId":"2000"},
|
||||
{"id":"2067","kind":"Struct","name":"PrefetchTrace","structTypeName":"_EX_FAST_REF","offset":912,"parentId":"2000","refId":"140","collapsed":true},
|
||||
{"id":"2068","kind":"Pointer64","name":"LockedPagesList","offset":920,"parentId":"2000"},
|
||||
{"id":"2069","kind":"Int64","name":"ReadOperationCount","offset":928,"parentId":"2000"},
|
||||
{"id":"2070","kind":"Int64","name":"WriteOperationCount","offset":936,"parentId":"2000"},
|
||||
{"id":"2071","kind":"Int64","name":"OtherOperationCount","offset":944,"parentId":"2000"},
|
||||
{"id":"2072","kind":"Int64","name":"ReadTransferCount","offset":952,"parentId":"2000"},
|
||||
{"id":"2073","kind":"Int64","name":"WriteTransferCount","offset":960,"parentId":"2000"},
|
||||
{"id":"2074","kind":"Int64","name":"OtherTransferCount","offset":968,"parentId":"2000"},
|
||||
{"id":"2075","kind":"UInt64","name":"CommitChargeLimit","offset":976,"parentId":"2000"},
|
||||
{"id":"2076","kind":"UInt64","name":"CommitCharge","offset":984,"parentId":"2000"},
|
||||
{"id":"2077","kind":"UInt64","name":"CommitChargePeak","offset":992,"parentId":"2000"},
|
||||
{"id":"2078","kind":"Struct","name":"Vm","structTypeName":"_MMSUPPORT_FULL","offset":1024,"parentId":"2000","refId":"350","collapsed":true},
|
||||
{"id":"2079","kind":"Struct","name":"MmProcessLinks","structTypeName":"_LIST_ENTRY","offset":1344,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2080","kind":"UInt32","name":"ModifiedPageCount","offset":1360,"parentId":"2000"},
|
||||
{"id":"2081","kind":"Int32","name":"ExitStatus","offset":1364,"parentId":"2000"},
|
||||
{"id":"2082","kind":"Struct","name":"VadRoot","structTypeName":"_RTL_AVL_TREE","offset":1368,"parentId":"2000","refId":"170","collapsed":true},
|
||||
{"id":"2083","kind":"Pointer64","name":"VadHint","offset":1376,"parentId":"2000"},
|
||||
{"id":"2084","kind":"UInt64","name":"VadCount","offset":1384,"parentId":"2000"},
|
||||
{"id":"2085","kind":"UInt64","name":"VadPhysicalPages","offset":1392,"parentId":"2000"},
|
||||
{"id":"2086","kind":"UInt64","name":"VadPhysicalPagesLimit","offset":1400,"parentId":"2000"},
|
||||
{"id":"2087","kind":"Struct","name":"AlpcContext","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":1408,"parentId":"2000","refId":"250","collapsed":true},
|
||||
{"id":"2088","kind":"Struct","name":"TimerResolutionLink","structTypeName":"_LIST_ENTRY","offset":1440,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2089","kind":"Pointer64","name":"TimerResolutionStackRecord","offset":1456,"parentId":"2000"},
|
||||
{"id":"2090","kind":"UInt32","name":"RequestedTimerResolution","offset":1464,"parentId":"2000"},
|
||||
{"id":"2091","kind":"UInt32","name":"SmallestTimerResolution","offset":1468,"parentId":"2000"},
|
||||
{"id":"2092","kind":"Int64","name":"ExitTime","offset":1472,"parentId":"2000"},
|
||||
{"id":"2093","kind":"Pointer64","name":"InvertedFunctionTable","offset":1480,"parentId":"2000"},
|
||||
{"id":"2094","kind":"Struct","name":"InvertedFunctionTableLock","structTypeName":"_EX_PUSH_LOCK","offset":1488,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2095","kind":"UInt32","name":"ActiveThreadsHighWatermark","offset":1496,"parentId":"2000"},
|
||||
{"id":"2096","kind":"UInt32","name":"LargePrivateVadCount","offset":1500,"parentId":"2000"},
|
||||
{"id":"2097","kind":"Struct","name":"ThreadListLock","structTypeName":"_EX_PUSH_LOCK","offset":1504,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2098","kind":"Pointer64","name":"WnfContext","offset":1512,"parentId":"2000"},
|
||||
{"id":"2099","kind":"Pointer64","name":"ServerSilo","offset":1520,"parentId":"2000"},
|
||||
{"id":"2100","kind":"UInt8","name":"SignatureLevel","offset":1528,"parentId":"2000"},
|
||||
{"id":"2101","kind":"UInt8","name":"SectionSignatureLevel","offset":1529,"parentId":"2000"},
|
||||
{"id":"2102","kind":"Struct","name":"Protection","structTypeName":"_PS_PROTECTION","offset":1530,"parentId":"2000","refId":"210","collapsed":true},
|
||||
{"id":"2103","kind":"Hex8","name":"HangCount:3 GhostCount:3 PrefilterException:1","offset":1531,"parentId":"2000"},
|
||||
{"id":"2104","kind":"Struct","name":"","classKeyword":"union","offset":1532,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2105","kind":"UInt32","name":"Flags3","offset":0,"parentId":"2104"},
|
||||
{"id":"2106","kind":"Hex32","name":"Minimal:1 ReplacingPageRoot:1 Crashed:1 JobVadsAreTracked:1 VadTrackingDisabled:1 AuxiliaryProcess:1 SubsystemProcess:1 IndirectCpuSets:1 RelinquishedCommit:1 HighGraphicsPriority:1 CommitFailLogged:1 ReserveFailLogged:1 SystemProcess:1","offset":0,"parentId":"2104"},
|
||||
{"id":"2107","kind":"Int32","name":"DeviceAsid","offset":1536,"parentId":"2000"},
|
||||
{"id":"2108","kind":"Pointer64","name":"SvmData","offset":1544,"parentId":"2000"},
|
||||
{"id":"2109","kind":"Struct","name":"SvmProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":1552,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2110","kind":"UInt64","name":"SvmLock","offset":1560,"parentId":"2000"},
|
||||
{"id":"2111","kind":"Struct","name":"SvmProcessDeviceListHead","structTypeName":"_LIST_ENTRY","offset":1568,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2112","kind":"UInt64","name":"LastFreezeInterruptTime","offset":1584,"parentId":"2000"},
|
||||
{"id":"2113","kind":"Pointer64","name":"DiskCounters","offset":1592,"parentId":"2000"},
|
||||
{"id":"2114","kind":"Pointer64","name":"PicoContext","offset":1600,"parentId":"2000"},
|
||||
{"id":"2115","kind":"Pointer64","name":"EnclaveTable","offset":1608,"parentId":"2000"},
|
||||
{"id":"2116","kind":"UInt64","name":"EnclaveNumber","offset":1616,"parentId":"2000"},
|
||||
{"id":"2117","kind":"Struct","name":"EnclaveLock","structTypeName":"_EX_PUSH_LOCK","offset":1624,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2118","kind":"UInt32","name":"HighPriorityFaultsAllowed","offset":1632,"parentId":"2000"},
|
||||
{"id":"2119","kind":"Pointer64","name":"EnergyContext","offset":1640,"parentId":"2000"},
|
||||
{"id":"2120","kind":"Pointer64","name":"VmContext","offset":1648,"parentId":"2000"},
|
||||
{"id":"2121","kind":"UInt64","name":"SequenceNumber","offset":1656,"parentId":"2000"},
|
||||
{"id":"2122","kind":"UInt64","name":"CreateInterruptTime","offset":1664,"parentId":"2000"},
|
||||
{"id":"2123","kind":"UInt64","name":"CreateUnbiasedInterruptTime","offset":1672,"parentId":"2000"},
|
||||
{"id":"2124","kind":"UInt64","name":"TotalUnbiasedFrozenTime","offset":1680,"parentId":"2000"},
|
||||
{"id":"2125","kind":"UInt64","name":"LastAppStateUpdateTime","offset":1688,"parentId":"2000"},
|
||||
{"id":"2126","kind":"Hex64","name":"LastAppStateUptime:61 LastAppState:3","offset":1696,"parentId":"2000"},
|
||||
{"id":"2127","kind":"UInt64","name":"SharedCommitCharge","offset":1704,"parentId":"2000"},
|
||||
{"id":"2128","kind":"Struct","name":"SharedCommitLock","structTypeName":"_EX_PUSH_LOCK","offset":1712,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2129","kind":"Struct","name":"SharedCommitLinks","structTypeName":"_LIST_ENTRY","offset":1720,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2130","kind":"UInt64","name":"AllowedCpuSets","offset":1736,"parentId":"2000"},
|
||||
{"id":"2131","kind":"UInt64","name":"DefaultCpuSets","offset":1744,"parentId":"2000"},
|
||||
{"id":"2132","kind":"Pointer64","name":"DiskIoAttribution","offset":1752,"parentId":"2000"},
|
||||
{"id":"2133","kind":"Pointer64","name":"DxgProcess","offset":1760,"parentId":"2000"},
|
||||
{"id":"2134","kind":"UInt32","name":"Win32KFilterSet","offset":1768,"parentId":"2000"},
|
||||
{"id":"2135","kind":"UInt16","name":"Machine","offset":1772,"parentId":"2000"},
|
||||
{"id":"2136","kind":"UInt8","name":"MmSlabIdentity","offset":1774,"parentId":"2000"},
|
||||
{"id":"2137","kind":"UInt8","name":"Spare0","offset":1775,"parentId":"2000"},
|
||||
{"id":"2138","kind":"Struct","name":"ProcessTimerDelay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","offset":1776,"parentId":"2000","refId":"220","collapsed":true},
|
||||
{"id":"2139","kind":"UInt32","name":"KTimerSets","offset":1784,"parentId":"2000"},
|
||||
{"id":"2140","kind":"UInt32","name":"KTimer2Sets","offset":1788,"parentId":"2000"},
|
||||
{"id":"2141","kind":"UInt32","name":"ThreadTimerSets","offset":1792,"parentId":"2000"},
|
||||
{"id":"2142","kind":"UInt64","name":"VirtualTimerListLock","offset":1800,"parentId":"2000"},
|
||||
{"id":"2143","kind":"Struct","name":"VirtualTimerListHead","structTypeName":"_LIST_ENTRY","offset":1808,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2144","kind":"Struct","name":"WakeChannel","structTypeName":"_WNF_STATE_NAME","offset":1824,"parentId":"2000","refId":"230","collapsed":true},
|
||||
{"id":"2145","kind":"Struct","name":"","classKeyword":"union","offset":1872,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2146","kind":"UInt32","name":"MitigationFlags","offset":0,"parentId":"2145"},
|
||||
{"id":"2147","kind":"Hex32","name":"ControlFlowGuardEnabled:1 ControlFlowGuardExportSuppressionEnabled:1 ControlFlowGuardStrict:1 DisallowStrippedImages:1 ForceRelocateImages:1 HighEntropyASLREnabled:1 StackRandomizationDisabled:1 ExtensionPointDisable:1 DisableDynamicCode:1","offset":0,"parentId":"2145"},
|
||||
{"id":"2148","kind":"Struct","name":"","classKeyword":"union","offset":1876,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2149","kind":"UInt32","name":"MitigationFlags2","offset":0,"parentId":"2148"},
|
||||
{"id":"2150","kind":"Hex32","name":"EnableExportAddressFilter:1 AuditExportAddressFilter:1 EnableRopStackPivot:1 AuditRopStackPivot:1 CetUserShadowStacks:1 SpeculativeStoreBypassDisable:1","offset":0,"parentId":"2148"},
|
||||
{"id":"2151","kind":"Pointer64","name":"PartitionObject","offset":1880,"parentId":"2000"},
|
||||
{"id":"2152","kind":"UInt64","name":"SecurityDomain","offset":1888,"parentId":"2000"},
|
||||
{"id":"2153","kind":"UInt64","name":"ParentSecurityDomain","offset":1896,"parentId":"2000"},
|
||||
{"id":"2154","kind":"Pointer64","name":"CoverageSamplerContext","offset":1904,"parentId":"2000"},
|
||||
{"id":"2155","kind":"Pointer64","name":"MmHotPatchContext","offset":1912,"parentId":"2000"},
|
||||
{"id":"2156","kind":"Struct","name":"DynamicEHContinuationTargetsTree","structTypeName":"_RTL_AVL_TREE","offset":1920,"parentId":"2000","refId":"170","collapsed":true},
|
||||
{"id":"2157","kind":"Struct","name":"DynamicEHContinuationTargetsLock","structTypeName":"_EX_PUSH_LOCK","offset":1928,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2158","kind":"Struct","name":"DynamicEnforcedCetCompatibleRanges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":1936,"parentId":"2000","refId":"240","collapsed":true},
|
||||
{"id":"2159","kind":"UInt32","name":"DisabledComponentFlags","offset":1952,"parentId":"2000"},
|
||||
{"id":"2160","kind":"Int32","name":"PageCombineSequence","offset":1956,"parentId":"2000"},
|
||||
{"id":"2161","kind":"Struct","name":"EnableOptionalXStateFeaturesLock","structTypeName":"_EX_PUSH_LOCK","offset":1960,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2162","kind":"Pointer64","name":"PathRedirectionHashes","offset":1968,"parentId":"2000"},
|
||||
{"id":"2163","kind":"Pointer64","name":"SyscallProvider","offset":1976,"parentId":"2000"},
|
||||
{"id":"2164","kind":"Struct","name":"SyscallProviderProcessLinks","structTypeName":"_LIST_ENTRY","offset":1984,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2165","kind":"Hex64","name":"SyscallProviderDispatchContext","offset":2000,"parentId":"2000"},
|
||||
{"id":"2166","kind":"Struct","name":"","classKeyword":"union","offset":2008,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2167","kind":"UInt32","name":"MitigationFlags3","offset":0,"parentId":"2166"},
|
||||
{"id":"2168","kind":"Hex32","name":"RestrictCoreSharing:1 DisallowFsctlSystemCalls:1 AuditDisallowFsctlSystemCalls:1 MitigationFlags3Spare:29","offset":0,"parentId":"2166"},
|
||||
{"id":"2169","kind":"Struct","name":"","classKeyword":"union","offset":2012,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2170","kind":"UInt32","name":"Flags4","offset":0,"parentId":"2169"},
|
||||
{"id":"2171","kind":"Hex32","name":"ThreadWasActive:1 MinimalTerminate:1 ImageExpansionDisable:1 SessionFirstProcess:1","offset":0,"parentId":"2169"},
|
||||
{"id":"2172","kind":"Struct","name":"","classKeyword":"union","offset":2016,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2173","kind":"UInt32","name":"SyscallUsage","offset":0,"parentId":"2172"},
|
||||
{"id":"2174","kind":"Hex32","name":"SystemModuleInformation:1 SystemModuleInformationEx:1 SystemLocksInformation:1 SystemHandleInformation:1 SystemExtendedHandleInformation:1","offset":0,"parentId":"2172"},
|
||||
{"id":"2175","kind":"Int32","name":"SupervisorDeviceAsid","offset":2020,"parentId":"2000"},
|
||||
{"id":"2176","kind":"Pointer64","name":"SupervisorSvmData","offset":2024,"parentId":"2000"},
|
||||
{"id":"2177","kind":"Pointer64","name":"NetworkCounters","offset":2032,"parentId":"2000"},
|
||||
{"id":"2178","kind":"Hex64","name":"Execution","offset":2040,"parentId":"2000"},
|
||||
{"id":"2179","kind":"Pointer64","name":"ThreadIndexTable","offset":2048,"parentId":"2000"},
|
||||
{"id":"2180","kind":"Struct","name":"FreezeWorkLinks","structTypeName":"_LIST_ENTRY","offset":2056,"parentId":"2000","refId":"100","collapsed":true}
|
||||
]
|
||||
}
|
||||
1316
src/examples/KUSER_SHARED_DATA.rcx
Normal file
1316
src/examples/KUSER_SHARED_DATA.rcx
Normal file
File diff suppressed because it is too large
Load Diff
616
src/examples/MMPFN.rcx
Normal file
616
src/examples/MMPFN.rcx
Normal file
@@ -0,0 +1,616 @@
|
||||
{
|
||||
"baseAddress": "FFFFCA8010000000",
|
||||
"nextId": "3000",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "100",
|
||||
"kind": "Struct",
|
||||
"name": "list_entry",
|
||||
"structTypeName": "_LIST_ENTRY",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "101",
|
||||
"kind": "Pointer64",
|
||||
"name": "Flink",
|
||||
"offset": 0,
|
||||
"parentId": "100",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "102",
|
||||
"kind": "Pointer64",
|
||||
"name": "Blink",
|
||||
"offset": 8,
|
||||
"parentId": "100",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "200",
|
||||
"kind": "Struct",
|
||||
"name": "balanced_node",
|
||||
"structTypeName": "_RTL_BALANCED_NODE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "210",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "200",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "211",
|
||||
"kind": "Pointer64",
|
||||
"name": "Left",
|
||||
"offset": 0,
|
||||
"parentId": "210",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "212",
|
||||
"kind": "Pointer64",
|
||||
"name": "Right",
|
||||
"offset": 8,
|
||||
"parentId": "210",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "220",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 16,
|
||||
"parentId": "200",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "221",
|
||||
"kind": "UInt64",
|
||||
"name": "ParentValue",
|
||||
"offset": 0,
|
||||
"parentId": "220"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "300",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte",
|
||||
"structTypeName": "_MMPTE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "301",
|
||||
"kind": "Struct",
|
||||
"name": "u",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "300",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "302",
|
||||
"kind": "UInt64",
|
||||
"name": "Long",
|
||||
"offset": 0,
|
||||
"parentId": "301"
|
||||
},
|
||||
{
|
||||
"id": "303",
|
||||
"kind": "Struct",
|
||||
"name": "Hard",
|
||||
"structTypeName": "_MMPTE_HARDWARE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "400",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "304",
|
||||
"kind": "Struct",
|
||||
"name": "Proto",
|
||||
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "600",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "305",
|
||||
"kind": "Struct",
|
||||
"name": "Soft",
|
||||
"structTypeName": "_MMPTE_SOFTWARE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "500",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "306",
|
||||
"kind": "Struct",
|
||||
"name": "Trans",
|
||||
"structTypeName": "_MMPTE_TRANSITION",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "700",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "307",
|
||||
"kind": "Struct",
|
||||
"name": "Subsect",
|
||||
"structTypeName": "_MMPTE_SUBSECTION",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "800",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "308",
|
||||
"kind": "Struct",
|
||||
"name": "TimeStamp",
|
||||
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "900",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "309",
|
||||
"kind": "Struct",
|
||||
"name": "List",
|
||||
"structTypeName": "_MMPTE_LIST",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "1000",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "400",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_hardware",
|
||||
"structTypeName": "_MMPTE_HARDWARE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "401",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Dirty1:1 Owner:1 WriteThrough:1 CacheDisable:1 Accessed:1 Dirty:1 LargePage:1 Global:1 CopyOnWrite:1 Unused:1 Write:1 PageFrameNumber:40 ReservedForSoftware:4 WsleAge:4 WsleProtection:3 NoExecute:1",
|
||||
"offset": 0,
|
||||
"parentId": "400"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "500",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_software",
|
||||
"structTypeName": "_MMPTE_SOFTWARE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "501",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 PageFileReserved:1 PageFileAllocated:1 ColdPage:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 UsedPageTableEntries:10 ShadowStack:1 OnStandbyLookaside:1 Unused:4 PageFileHigh:32",
|
||||
"offset": 0,
|
||||
"parentId": "500"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "600",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_prototype",
|
||||
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "601",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 DemandFillProto:1 HiberVerifyConverted:1 ReadOnly:1 SwizzleBit:1 Protection:5 Prototype:1 Combined:1 Unused1:4 ProtoAddress:48",
|
||||
"offset": 0,
|
||||
"parentId": "600"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "700",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_transition",
|
||||
"structTypeName": "_MMPTE_TRANSITION",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "701",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Write:1 OnStandbyLookaside:1 IoTracker:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFrameNumber:40 Unused:12",
|
||||
"offset": 0,
|
||||
"parentId": "700"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "800",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_subsection",
|
||||
"structTypeName": "_MMPTE_SUBSECTION",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "801",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Unused0:2 OnStandbyLookaside:1 SwizzleBit:1 Protection:5 Prototype:1 ColdPage:1 Unused2:3 ExecutePrivilege:1 SubsectionAddress:48",
|
||||
"offset": 0,
|
||||
"parentId": "800"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "900",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_timestamp",
|
||||
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "901",
|
||||
"kind": "Hex64",
|
||||
"name": "MustBeZero:1 Unused:3 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 Reserved:16 GlobalTimeStamp:32",
|
||||
"offset": 0,
|
||||
"parentId": "900"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1000",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_list",
|
||||
"structTypeName": "_MMPTE_LIST",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1001",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 OneEntry:1 filler0:2 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 filler1:13 NextEntry:39",
|
||||
"offset": 0,
|
||||
"parentId": "1000"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1100",
|
||||
"kind": "Struct",
|
||||
"name": "mipfnflink",
|
||||
"structTypeName": "_MIPFNFLINK",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1101",
|
||||
"kind": "Hex64",
|
||||
"name": "Flink",
|
||||
"offset": 0,
|
||||
"parentId": "1100"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1200",
|
||||
"kind": "Struct",
|
||||
"name": "mipfnblink",
|
||||
"structTypeName": "_MIPFNBLINK",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1201",
|
||||
"kind": "Hex64",
|
||||
"name": "Blink",
|
||||
"offset": 0,
|
||||
"parentId": "1200"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1300",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfnentry1",
|
||||
"structTypeName": "_MMPFNENTRY1",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1301",
|
||||
"kind": "Hex8",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1300"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1400",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfnentry3",
|
||||
"structTypeName": "_MMPFNENTRY3",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1401",
|
||||
"kind": "Hex8",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1400"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1500",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags",
|
||||
"structTypeName": "_MI_PFN_FLAGS",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1501",
|
||||
"kind": "Hex32",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1500"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1600",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags4",
|
||||
"structTypeName": "_MI_PFN_FLAGS4",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1601",
|
||||
"kind": "Hex64",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1600"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1700",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags5",
|
||||
"structTypeName": "_MI_PFN_FLAGS5",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1701",
|
||||
"kind": "Hex32",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1700"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2000",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfn",
|
||||
"structTypeName": "_MMPFN",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2001",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "2000",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2010",
|
||||
"kind": "Struct",
|
||||
"name": "ListEntry",
|
||||
"structTypeName": "_LIST_ENTRY",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2011",
|
||||
"kind": "Struct",
|
||||
"name": "TreeNode",
|
||||
"structTypeName": "_RTL_BALANCED_NODE",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2012",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2013",
|
||||
"kind": "Struct",
|
||||
"name": "u1",
|
||||
"structTypeName": "_MIPFNFLINK",
|
||||
"offset": 0,
|
||||
"parentId": "2012",
|
||||
"refId": "1100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2014",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 8,
|
||||
"parentId": "2012",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2015",
|
||||
"kind": "Pointer64",
|
||||
"name": "PteAddress",
|
||||
"offset": 0,
|
||||
"parentId": "2014",
|
||||
"refId": "300",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2016",
|
||||
"kind": "UInt64",
|
||||
"name": "PteLong",
|
||||
"offset": 0,
|
||||
"parentId": "2014"
|
||||
},
|
||||
{
|
||||
"id": "2017",
|
||||
"kind": "Struct",
|
||||
"name": "OriginalPte",
|
||||
"structTypeName": "_MMPTE",
|
||||
"offset": 16,
|
||||
"parentId": "2012",
|
||||
"refId": "300",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2020",
|
||||
"kind": "Struct",
|
||||
"name": "u2",
|
||||
"structTypeName": "_MIPFNBLINK",
|
||||
"offset": 24,
|
||||
"parentId": "2000",
|
||||
"refId": "1200",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2030",
|
||||
"kind": "Struct",
|
||||
"name": "u3",
|
||||
"classKeyword": "union",
|
||||
"offset": 32,
|
||||
"parentId": "2000",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2031",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"offset": 0,
|
||||
"parentId": "2030",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2032",
|
||||
"kind": "UInt16",
|
||||
"name": "ReferenceCount",
|
||||
"offset": 0,
|
||||
"parentId": "2031"
|
||||
},
|
||||
{
|
||||
"id": "2033",
|
||||
"kind": "Struct",
|
||||
"name": "e1",
|
||||
"structTypeName": "_MMPFNENTRY1",
|
||||
"offset": 2,
|
||||
"parentId": "2031",
|
||||
"refId": "1300",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2034",
|
||||
"kind": "Struct",
|
||||
"name": "e4",
|
||||
"structTypeName": "_MI_PFN_FLAGS",
|
||||
"offset": 0,
|
||||
"parentId": "2030",
|
||||
"refId": "1500",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2040",
|
||||
"kind": "Struct",
|
||||
"name": "u5",
|
||||
"structTypeName": "_MI_PFN_FLAGS5",
|
||||
"offset": 36,
|
||||
"parentId": "2000",
|
||||
"refId": "1700",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2050",
|
||||
"kind": "Struct",
|
||||
"name": "u4",
|
||||
"structTypeName": "_MI_PFN_FLAGS4",
|
||||
"offset": 40,
|
||||
"parentId": "2000",
|
||||
"refId": "1600",
|
||||
"collapsed": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
{
|
||||
"baseAddress": "400000",
|
||||
"nextId": "29",
|
||||
"nodes": [
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "1",
|
||||
"kind": "Struct",
|
||||
"name": "aBall",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"strLen": 64,
|
||||
"structTypeName": "ball"
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "2",
|
||||
"kind": "Hex64",
|
||||
"name": "field_00",
|
||||
"offset": 0,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "3",
|
||||
"kind": "Hex64",
|
||||
"name": "field_08",
|
||||
"offset": 8,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "4",
|
||||
"kind": "Vec4",
|
||||
"name": "position",
|
||||
"offset": 16,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "5",
|
||||
"kind": "Vec3",
|
||||
"name": "velocity",
|
||||
"offset": 32,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "6",
|
||||
"kind": "Hex32",
|
||||
"name": "field_2C",
|
||||
"offset": 44,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "7",
|
||||
"kind": "Float",
|
||||
"name": "speed",
|
||||
"offset": 48,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "8",
|
||||
"kind": "UInt32",
|
||||
"name": "color",
|
||||
"offset": 52,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "9",
|
||||
"kind": "Float",
|
||||
"name": "radius",
|
||||
"offset": 56,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "10",
|
||||
"kind": "Hex32",
|
||||
"name": "field_3C",
|
||||
"offset": 60,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "11",
|
||||
"kind": "Float",
|
||||
"name": "mass",
|
||||
"offset": 64,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "12",
|
||||
"kind": "Hex64",
|
||||
"name": "field_44",
|
||||
"offset": 68,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "13",
|
||||
"kind": "Bool",
|
||||
"name": "bouncy",
|
||||
"offset": 76,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "14",
|
||||
"kind": "Hex8",
|
||||
"name": "field_4D",
|
||||
"offset": 77,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "15",
|
||||
"kind": "Hex16",
|
||||
"name": "field_4E",
|
||||
"offset": 78,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "16",
|
||||
"kind": "UInt32",
|
||||
"name": "color",
|
||||
"offset": 80,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "17",
|
||||
"kind": "Hex32",
|
||||
"name": "field_54",
|
||||
"offset": 84,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "18",
|
||||
"kind": "Hex64",
|
||||
"name": "field_58",
|
||||
"offset": 88,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "19",
|
||||
"kind": "Hex64",
|
||||
"name": "field_60",
|
||||
"offset": 96,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "20",
|
||||
"kind": "Struct",
|
||||
"name": "aPhysics",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"strLen": 64,
|
||||
"structTypeName": "Physics"
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "21",
|
||||
"kind": "Hex64",
|
||||
"name": "field_00",
|
||||
"offset": 0,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "22",
|
||||
"kind": "Hex64",
|
||||
"name": "field_08",
|
||||
"offset": 8,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "23",
|
||||
"kind": "Hex64",
|
||||
"name": "field_10",
|
||||
"offset": 16,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "24",
|
||||
"kind": "Hex64",
|
||||
"name": "field_18",
|
||||
"offset": 24,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "25",
|
||||
"kind": "Hex64",
|
||||
"name": "field_20",
|
||||
"offset": 32,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": true,
|
||||
"elementKind": "UInt8",
|
||||
"id": "26",
|
||||
"kind": "Pointer64",
|
||||
"name": "physics",
|
||||
"offset": 104,
|
||||
"parentId": "1",
|
||||
"refId": "20",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 4,
|
||||
"collapsed": false,
|
||||
"elementKind": "Float",
|
||||
"id": "27",
|
||||
"kind": "Array",
|
||||
"name": "scores",
|
||||
"offset": 112,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 2,
|
||||
"collapsed": false,
|
||||
"elementKind": "Struct",
|
||||
"id": "28",
|
||||
"kind": "Array",
|
||||
"name": "materials",
|
||||
"offset": 128,
|
||||
"parentId": "1",
|
||||
"refId": "20",
|
||||
"strLen": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
198
src/format.cpp
198
src/format.cpp
@@ -1,4 +1,5 @@
|
||||
#include "core.h"
|
||||
#include "addressparser.h"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
@@ -22,6 +23,14 @@ static QString fit(QString s, int w) {
|
||||
return s.leftJustified(w, ' ');
|
||||
}
|
||||
|
||||
// Like fit() but overflows instead of truncating: if s exceeds w, return full string
|
||||
static QString fitOverflow(const QString& s, int w) {
|
||||
if (w <= 0) return {};
|
||||
if (s.size() <= w)
|
||||
return s.leftJustified(w, ' ');
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Type name ──
|
||||
|
||||
// Override seam: injectable type-name provider
|
||||
@@ -112,15 +121,8 @@ QString fmtDouble(double v) {
|
||||
}
|
||||
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
||||
|
||||
QString fmtPointer32(uint32_t v) {
|
||||
if (v == 0) return QStringLiteral("-> NULL");
|
||||
return QStringLiteral("-> ") + hexVal(v);
|
||||
}
|
||||
|
||||
QString fmtPointer64(uint64_t v) {
|
||||
if (v == 0) return QStringLiteral("-> NULL");
|
||||
return QStringLiteral("-> ") + hexVal(v);
|
||||
}
|
||||
QString fmtPointer32(uint32_t v) { return hexVal(v); }
|
||||
QString fmtPointer64(uint64_t v) { return hexVal(v); }
|
||||
|
||||
// ── Indentation ──
|
||||
|
||||
@@ -139,20 +141,25 @@ QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDig
|
||||
// ── Struct type name (for width calculation) ──
|
||||
|
||||
QString structTypeName(const Node& node) {
|
||||
// Full type string: "struct TypeName" or just "struct" if no typename
|
||||
QString base = typeName(node.kind).trimmed(); // "struct"
|
||||
// Named types: just the type name (e.g. "_LIST_ENTRY")
|
||||
// Anonymous: just the keyword (e.g. "union", "struct")
|
||||
if (!node.structTypeName.isEmpty())
|
||||
return base + QStringLiteral(" ") + node.structTypeName;
|
||||
return base;
|
||||
return node.structTypeName;
|
||||
return node.resolvedClassKeyword();
|
||||
}
|
||||
|
||||
// ── Struct header / footer ──
|
||||
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
|
||||
// Columnar format: <type> <name> { (or no brace when collapsed)
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(structTypeName(node), colType);
|
||||
QString rawType = structTypeName(node);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
if (node.name.isEmpty()) {
|
||||
// Anonymous struct/union: "union {" with no column padding
|
||||
return ind + rawType + SEP + suffix;
|
||||
}
|
||||
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
|
||||
@@ -162,9 +169,10 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
|
||||
// ── Array header ──
|
||||
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
|
||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName) {
|
||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen, elemStructName), colType);
|
||||
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
||||
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
@@ -173,10 +181,16 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
|
||||
|
||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||
const Provider& prov, uint64_t addr,
|
||||
const QString& ptrTypeName, int colType, int colName) {
|
||||
const QString& ptrTypeName, int colType, int colName,
|
||||
bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(ptrTypeName, colType);
|
||||
bool overflow = compact && ptrTypeName.size() > colType;
|
||||
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
|
||||
if (collapsed) {
|
||||
if (overflow) {
|
||||
// Overflow: no column padding
|
||||
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
|
||||
}
|
||||
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
||||
QString name = fit(node.name, colName);
|
||||
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
||||
@@ -262,15 +276,55 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Pointer64: {
|
||||
uint64_t val = prov.readU64(addr);
|
||||
// Primitive pointer: dereference and show target value
|
||||
// (hex/ptr/fnptr targets fall through to plain void* display)
|
||||
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) {
|
||||
uint64_t target = val;
|
||||
for (int d = 1; d < node.ptrDepth && target != 0; d++)
|
||||
target = prov.isReadable(target, 8) ? prov.readU64(target) : 0;
|
||||
if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) {
|
||||
// Create a temporary node of the target kind to format the value
|
||||
Node tmp;
|
||||
tmp.kind = node.elementKind;
|
||||
tmp.strLen = node.strLen;
|
||||
QString derefVal = readValueImpl(tmp, prov, target, 0, mode);
|
||||
if (display) {
|
||||
QString arrow = QStringLiteral("-> ");
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty())
|
||||
return arrow + derefVal + QStringLiteral(" // ") + sym;
|
||||
return arrow + derefVal;
|
||||
}
|
||||
return derefVal;
|
||||
}
|
||||
if (!display) return rawHex(val, 16);
|
||||
return fmtPointer64(val);
|
||||
}
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr32: {
|
||||
uint32_t val = prov.readU32(addr);
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr64: {
|
||||
uint64_t val = prov.readU64(addr);
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Vec2:
|
||||
@@ -293,7 +347,6 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
line += QStringLiteral("]");
|
||||
return line;
|
||||
}
|
||||
case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
||||
case NodeKind::UTF8: {
|
||||
QByteArray bytes = prov.readBytes(addr, node.strLen);
|
||||
int end = bytes.indexOf('\0');
|
||||
@@ -326,12 +379,22 @@ QString readValue(const Node& node, const Provider& prov,
|
||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
uint64_t addr, int depth, int subLine,
|
||||
const QString& comment, int colType, int colName,
|
||||
const QString& typeOverride) {
|
||||
const QString& typeOverride, bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
|
||||
QString name = fit(node.name, colName);
|
||||
|
||||
// Compute raw type string for overflow detection
|
||||
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
|
||||
bool overflow = compact && rawType.size() > colType;
|
||||
|
||||
QString type = overflow ? fitOverflow(rawType, colType)
|
||||
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
|
||||
: fit(typeOverride, colType));
|
||||
QString name = overflow ? node.name : fit(node.name, colName);
|
||||
|
||||
// Effective column width for this line (accounts for overflow, clamped to hard max)
|
||||
int effectiveColType = overflow ? rawType.size() : colType;
|
||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||
const int prefixW = colType + colName + 2 * kSepWidth;
|
||||
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
|
||||
|
||||
// Comment suffix (only present when a comment is provided; no trailing padding)
|
||||
QString cmtSuffix = comment.isEmpty() ? QString()
|
||||
@@ -344,21 +407,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||
}
|
||||
|
||||
// Hex nodes and Padding: hex byte preview (ASCII padded to colName to align with value column)
|
||||
// Hex nodes: hex byte preview (ASCII padded to colName to align with value column)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
const int totalSz = qMax(1, node.arrayLen);
|
||||
const int lineOff = subLine * 8;
|
||||
const int lineBytes = qMin(8, totalSz - lineOff);
|
||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||
QString ascii = bytesToAscii(b, lineBytes).leftJustified(colName, ' ');
|
||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||
if (subLine == 0)
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
// Hex8..Hex64: single line, ASCII padded to colName so hex column aligns with value column
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(addr, sz)
|
||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||
@@ -367,7 +417,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
|
||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
QString val = overflow ? readValue(node, prov, addr, subLine)
|
||||
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||
}
|
||||
|
||||
@@ -557,6 +608,14 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
|
||||
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
||||
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::FuncPtr32: {
|
||||
uint val = stripHex(s).toUInt(ok, 16);
|
||||
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::FuncPtr64: {
|
||||
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
||||
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::UTF8: {
|
||||
*ok = true;
|
||||
if (s.startsWith('"') && s.endsWith('"'))
|
||||
@@ -585,7 +644,8 @@ QString validateValue(NodeKind kind, const QString& text) {
|
||||
|
||||
// For integer/hex types, validate character set first
|
||||
bool isHexKind = (kind >= NodeKind::Hex8 && kind <= NodeKind::Hex64)
|
||||
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64;
|
||||
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64
|
||||
|| kind == NodeKind::FuncPtr32 || kind == NodeKind::FuncPtr64;
|
||||
bool isIntKind = (kind >= NodeKind::Int8 && kind <= NodeKind::UInt64);
|
||||
|
||||
if (isHexKind || isIntKind) {
|
||||
@@ -629,43 +689,41 @@ QString validateValue(NodeKind kind, const QString& text) {
|
||||
return QStringLiteral("invalid");
|
||||
}
|
||||
|
||||
// ── Base address validation (supports simple +/- equations) ──
|
||||
// ── Base address validation (delegates to AddressParser) ──
|
||||
|
||||
QString validateBaseAddress(const QString& text) {
|
||||
QString s = text.trimmed();
|
||||
if (s.isEmpty()) return QStringLiteral("empty");
|
||||
//s.remove('`');
|
||||
return AddressParser::validate(s);
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
bool firstTerm = true;
|
||||
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
|
||||
QString ind = indent(depth);
|
||||
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
|
||||
}
|
||||
|
||||
while (pos < s.size()) {
|
||||
// Skip whitespace
|
||||
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||
if (pos >= s.size()) break;
|
||||
// ── Bitfield member formatting ──
|
||||
|
||||
// Check for +/- operator (except first term)
|
||||
if (!firstTerm) {
|
||||
if (s[pos] == '+' || s[pos] == '-') pos++;
|
||||
else return QStringLiteral("invalid '%1'").arg(s[pos]);
|
||||
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||
}
|
||||
|
||||
// Skip 0x prefix if present
|
||||
if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'))
|
||||
pos += 2;
|
||||
|
||||
// Must have at least one hex digit
|
||||
int numStart = pos;
|
||||
while (pos < s.size() && (s[pos].isDigit() ||
|
||||
(s[pos] >= 'a' && s[pos] <= 'f') ||
|
||||
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
|
||||
|
||||
if (pos == numStart) return QStringLiteral("invalid");
|
||||
|
||||
firstTerm = false;
|
||||
uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
NodeKind containerKind,
|
||||
uint8_t bitOffset, uint8_t bitWidth) {
|
||||
uint64_t container = 0;
|
||||
switch (containerKind) {
|
||||
case NodeKind::Hex8: container = prov.readU8(addr); break;
|
||||
case NodeKind::Hex16: container = prov.readU16(addr); break;
|
||||
case NodeKind::Hex32: container = prov.readU32(addr); break;
|
||||
default: container = prov.readU64(addr); break;
|
||||
}
|
||||
if (bitWidth >= 64) return container >> bitOffset;
|
||||
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
|
||||
}
|
||||
|
||||
return {};
|
||||
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
|
||||
uint64_t value, int depth, int nameW) {
|
||||
QString ind = indent(depth);
|
||||
return ind + name.leftJustified(nameW)
|
||||
+ QStringLiteral(" : %1 = %2").arg(bitWidth).arg(value);
|
||||
}
|
||||
|
||||
} // namespace rcx::fmt
|
||||
|
||||
@@ -44,13 +44,14 @@ static QString cTypeName(NodeKind kind) {
|
||||
case NodeKind::Bool: return QStringLiteral("bool");
|
||||
case NodeKind::Pointer32: return QStringLiteral("uint32_t");
|
||||
case NodeKind::Pointer64: return QStringLiteral("uint64_t");
|
||||
case NodeKind::FuncPtr32: return QStringLiteral("uint32_t");
|
||||
case NodeKind::FuncPtr64: return QStringLiteral("uint64_t");
|
||||
case NodeKind::Vec2: return QStringLiteral("float");
|
||||
case NodeKind::Vec3: return QStringLiteral("float");
|
||||
case NodeKind::Vec4: return QStringLiteral("float");
|
||||
case NodeKind::Mat4x4: return QStringLiteral("float");
|
||||
case NodeKind::UTF8: return QStringLiteral("char");
|
||||
case NodeKind::UTF16: return QStringLiteral("wchar_t");
|
||||
case NodeKind::Padding: return QStringLiteral("uint8_t");
|
||||
default: return QStringLiteral("uint8_t");
|
||||
}
|
||||
}
|
||||
@@ -123,8 +124,6 @@ static QString emitField(GenContext& ctx, const Node& node) {
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
||||
case NodeKind::UTF16:
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
||||
case NodeKind::Padding:
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)) + oc;
|
||||
case NodeKind::Pointer32: {
|
||||
if (node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
@@ -146,6 +145,10 @@ static QString emitField(GenContext& ctx, const Node& node) {
|
||||
}
|
||||
return QStringLiteral(" void* %1;").arg(name) + oc;
|
||||
}
|
||||
case NodeKind::FuncPtr32:
|
||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
||||
case NodeKind::FuncPtr64:
|
||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
||||
default:
|
||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
|
||||
}
|
||||
@@ -169,7 +172,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
auto emitPadRun = [&](int offset, int size) {
|
||||
if (size <= 0) return;
|
||||
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
|
||||
.arg(ctx.cType(NodeKind::Padding))
|
||||
.arg(QStringLiteral("uint8_t"))
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(size, 16).toUpper())
|
||||
.arg(offsetComment(offset));
|
||||
@@ -312,7 +315,8 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
&& !ctx.forwardDeclared.contains(child.refId)) {
|
||||
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
||||
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
|
||||
if (fwdKw == QStringLiteral("enum")) fwdKw = QStringLiteral("struct");
|
||||
if (fwdKw == QStringLiteral("enum") && ctx.tree.nodes[refIdx].enumMembers.isEmpty())
|
||||
fwdKw = QStringLiteral("struct");
|
||||
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
||||
ctx.forwardDeclared.insert(child.refId);
|
||||
}
|
||||
@@ -324,7 +328,21 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
QString kw = node.resolvedClassKeyword();
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
|
||||
|
||||
// Enum with members: emit as proper C enum
|
||||
if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||
ctx.output += QStringLiteral("enum %1 {\n").arg(typeName);
|
||||
for (const auto& m : node.enumMembers) {
|
||||
ctx.output += QStringLiteral(" %1 = %2,\n")
|
||||
.arg(sanitizeIdent(m.first))
|
||||
.arg(m.second);
|
||||
}
|
||||
ctx.output += QStringLiteral("};\n\n");
|
||||
ctx.visiting.remove(structId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum without members: fallback
|
||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
||||
|
||||
emitStructBody(ctx, structId);
|
||||
|
||||
222
src/imports/export_reclass_xml.cpp
Normal file
222
src/imports/export_reclass_xml.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "export_reclass_xml.h"
|
||||
#include <QFile>
|
||||
#include <QXmlStreamWriter>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Reverse type map: NodeKind -> ReClassEx V2016 XML Type integer
|
||||
static int xmlTypeForKind(NodeKind kind) {
|
||||
switch (kind) {
|
||||
case NodeKind::Struct: return 1; // ClassInstance
|
||||
case NodeKind::Hex32: return 4;
|
||||
case NodeKind::Hex64: return 5;
|
||||
case NodeKind::Hex16: return 6;
|
||||
case NodeKind::Hex8: return 7;
|
||||
case NodeKind::Pointer64: return 8; // ClassPointer
|
||||
case NodeKind::Pointer32: return 8;
|
||||
case NodeKind::Int64: return 9;
|
||||
case NodeKind::Int32: return 10;
|
||||
case NodeKind::Int16: return 11;
|
||||
case NodeKind::Int8: return 12;
|
||||
case NodeKind::Float: return 13;
|
||||
case NodeKind::Double: return 14;
|
||||
case NodeKind::UInt32: return 15;
|
||||
case NodeKind::UInt16: return 16;
|
||||
case NodeKind::UInt8: return 17;
|
||||
case NodeKind::UInt64: return 32;
|
||||
case NodeKind::UTF8: return 18;
|
||||
case NodeKind::UTF16: return 19;
|
||||
case NodeKind::Bool: return 17; // No native bool in ReClass, map to UInt8
|
||||
case NodeKind::Vec2: return 22;
|
||||
case NodeKind::Vec3: return 23;
|
||||
case NodeKind::Vec4: return 24;
|
||||
case NodeKind::Mat4x4: return 25;
|
||||
case NodeKind::Array: return 27; // ClassInstanceArray
|
||||
}
|
||||
return 7; // fallback to Hex8
|
||||
}
|
||||
|
||||
static int nodeSizeForExport(const Node& node) {
|
||||
switch (node.kind) {
|
||||
case NodeKind::UTF8: return node.strLen;
|
||||
case NodeKind::UTF16: return node.strLen * 2;
|
||||
case NodeKind::Array: {
|
||||
int elemSz = sizeForKind(node.elementKind);
|
||||
return node.arrayLen * (elemSz > 0 ? elemSz : 0);
|
||||
}
|
||||
default: return sizeForKind(node.kind);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a struct type name from a node ID
|
||||
static QString resolveStructName(const NodeTree& tree, uint64_t refId) {
|
||||
int idx = tree.indexOfId(refId);
|
||||
if (idx < 0) return {};
|
||||
const Node& ref = tree.nodes[idx];
|
||||
if (!ref.structTypeName.isEmpty()) return ref.structTypeName;
|
||||
return ref.name;
|
||||
}
|
||||
|
||||
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg) {
|
||||
if (tree.nodes.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No nodes to export");
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file for writing: ") + filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build child map
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
QXmlStreamWriter xml(&file);
|
||||
xml.setAutoFormatting(true);
|
||||
xml.setAutoFormattingIndent(4);
|
||||
xml.writeStartDocument();
|
||||
|
||||
xml.writeStartElement(QStringLiteral("ReClass"));
|
||||
xml.writeComment(QStringLiteral("ReClassEx"));
|
||||
|
||||
// Get root structs
|
||||
QVector<int> roots = childMap.value(0);
|
||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
int classCount = 0;
|
||||
|
||||
for (int ri : roots) {
|
||||
const Node& root = tree.nodes[ri];
|
||||
if (root.kind != NodeKind::Struct) continue;
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Class"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), root.name.isEmpty() ? root.structTypeName : root.name);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("28"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
xml.writeAttribute(QStringLiteral("Offset"), QStringLiteral("0"));
|
||||
xml.writeAttribute(QStringLiteral("strOffset"), QStringLiteral("0"));
|
||||
xml.writeAttribute(QStringLiteral("Code"), QString());
|
||||
|
||||
// Get children sorted by offset
|
||||
QVector<int> children = childMap.value(root.id);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
int i = 0;
|
||||
while (i < children.size()) {
|
||||
const Node& child = tree.nodes[children[i]];
|
||||
|
||||
// Bitfield container: export as hex node (ReClassEx has no bitfield concept)
|
||||
if (child.kind == NodeKind::Struct
|
||||
&& child.resolvedClassKeyword() == QStringLiteral("bitfield")) {
|
||||
int sz = child.byteSize();
|
||||
if (sz <= 0) sz = 4;
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), child.name);
|
||||
NodeKind hexKind = (sz <= 1) ? NodeKind::Hex8 : (sz <= 2) ? NodeKind::Hex16
|
||||
: (sz <= 4) ? NodeKind::Hex32 : NodeKind::Hex64;
|
||||
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(hexKind)));
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(sz));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QStringLiteral("bitfield"));
|
||||
xml.writeEndElement();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collapse consecutive hex nodes into a single Custom node (Type=21)
|
||||
if (isHexNode(child.kind)) {
|
||||
int runStart = child.offset;
|
||||
int runEnd = child.offset + child.byteSize();
|
||||
int j = i + 1;
|
||||
while (j < children.size()) {
|
||||
const Node& next = tree.nodes[children[j]];
|
||||
if (!isHexNode(next.kind)) break;
|
||||
if (next.offset < runEnd) break; // overlap
|
||||
runEnd = next.offset + next.byteSize();
|
||||
j++;
|
||||
}
|
||||
int totalSize = runEnd - runStart;
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
// Use first hex node's name if it's a single node, otherwise generate
|
||||
QString hexName = (j - i == 1 && !child.name.isEmpty()) ? child.name : QString();
|
||||
xml.writeAttribute(QStringLiteral("Name"), hexName);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("21")); // Custom
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(totalSize));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
xml.writeEndElement(); // Node
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), child.name);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(child.kind)));
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(nodeSizeForExport(child)));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
|
||||
// Pointer with target
|
||||
if ((child.kind == NodeKind::Pointer64 || child.kind == NodeKind::Pointer32) && child.refId != 0) {
|
||||
QString target = resolveStructName(tree, child.refId);
|
||||
if (!target.isEmpty())
|
||||
xml.writeAttribute(QStringLiteral("Pointer"), target);
|
||||
}
|
||||
|
||||
// Embedded struct instance
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
QString instName = child.structTypeName.isEmpty() ? child.name : child.structTypeName;
|
||||
xml.writeAttribute(QStringLiteral("Instance"), instName);
|
||||
}
|
||||
|
||||
// Array: Total attribute and child <Array> element
|
||||
if (child.kind == NodeKind::Array) {
|
||||
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||
|
||||
// Resolve element type name
|
||||
QString elemName;
|
||||
if (child.elementKind == NodeKind::Struct && !child.structTypeName.isEmpty()) {
|
||||
elemName = child.structTypeName;
|
||||
} else if (child.refId != 0) {
|
||||
elemName = resolveStructName(tree, child.refId);
|
||||
}
|
||||
if (elemName.isEmpty())
|
||||
elemName = kindToString(child.elementKind);
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Array"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), elemName);
|
||||
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||
xml.writeEndElement(); // Array
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // Node
|
||||
i++;
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // Class
|
||||
classCount++;
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // ReClass
|
||||
xml.writeEndDocument();
|
||||
file.close();
|
||||
|
||||
if (classCount == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No struct classes found to export");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
10
src/imports/export_reclass_xml.h
Normal file
10
src/imports/export_reclass_xml.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Export a NodeTree to ReClass .NET / ReClassEx compatible XML format.
|
||||
// Returns true on success; populates errorMsg on failure if non-null.
|
||||
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
1147
src/imports/import_pdb.cpp
Normal file
1147
src/imports/import_pdb.cpp
Normal file
File diff suppressed because it is too large
Load Diff
35
src/imports/import_pdb.h
Normal file
35
src/imports/import_pdb.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include <QVector>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct PdbTypeInfo {
|
||||
uint32_t typeIndex; // TPI type index
|
||||
QString name; // struct/class/union/enum name
|
||||
uint64_t size; // sizeof in bytes
|
||||
int childCount; // direct member count
|
||||
bool isUnion; // union vs struct/class
|
||||
bool isEnum = false; // enum type
|
||||
};
|
||||
|
||||
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
|
||||
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Phase 2: Import selected types with full recursive child types.
|
||||
// progressCb is called with (current, total) for each top-level type;
|
||||
// return false from the callback to cancel the import.
|
||||
using ProgressCb = std::function<bool(int current, int total)>;
|
||||
NodeTree importPdbSelected(const QString& pdbPath,
|
||||
const QVector<uint32_t>& typeIndices,
|
||||
QString* errorMsg = nullptr,
|
||||
ProgressCb progressCb = {});
|
||||
|
||||
// Legacy single-call API: import one struct by name (or all if filter empty).
|
||||
NodeTree importPdb(const QString& pdbPath,
|
||||
const QString& structFilter = {},
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
184
src/imports/import_pdb_dialog.cpp
Normal file
184
src/imports/import_pdb_dialog.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "import_pdb_dialog.h"
|
||||
#include "import_pdb.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QListWidget>
|
||||
#include <QLabel>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QApplication>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
PdbImportDialog::PdbImportDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("Import from PDB");
|
||||
resize(520, 480);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
|
||||
// PDB path row
|
||||
auto* pathRow = new QHBoxLayout;
|
||||
pathRow->addWidget(new QLabel("PDB File:"));
|
||||
m_pathEdit = new QLineEdit;
|
||||
m_pathEdit->setPlaceholderText("Select a PDB file...");
|
||||
pathRow->addWidget(m_pathEdit);
|
||||
m_browseBtn = new QPushButton("...");
|
||||
m_browseBtn->setFixedWidth(32);
|
||||
pathRow->addWidget(m_browseBtn);
|
||||
layout->addLayout(pathRow);
|
||||
|
||||
// Filter row
|
||||
auto* filterRow = new QHBoxLayout;
|
||||
filterRow->addWidget(new QLabel("Filter:"));
|
||||
m_filterEdit = new QLineEdit;
|
||||
m_filterEdit->setPlaceholderText("Type name filter...");
|
||||
m_filterEdit->setEnabled(false);
|
||||
filterRow->addWidget(m_filterEdit);
|
||||
layout->addLayout(filterRow);
|
||||
|
||||
// Select all checkbox
|
||||
m_selectAll = new QCheckBox("Select All");
|
||||
m_selectAll->setEnabled(false);
|
||||
layout->addWidget(m_selectAll);
|
||||
|
||||
// Type list
|
||||
m_typeList = new QListWidget;
|
||||
m_typeList->setEnabled(false);
|
||||
layout->addWidget(m_typeList);
|
||||
|
||||
// Count label
|
||||
m_countLabel = new QLabel("No PDB loaded");
|
||||
layout->addWidget(m_countLabel);
|
||||
|
||||
// Buttons
|
||||
m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setText("Import");
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
layout->addWidget(m_buttons);
|
||||
|
||||
connect(m_browseBtn, &QPushButton::clicked, this, &PdbImportDialog::browsePdb);
|
||||
connect(m_pathEdit, &QLineEdit::returnPressed, this, &PdbImportDialog::loadPdb);
|
||||
connect(m_filterEdit, &QLineEdit::textChanged, this, &PdbImportDialog::filterChanged);
|
||||
connect(m_selectAll, &QCheckBox::toggled, this, &PdbImportDialog::selectAllToggled);
|
||||
connect(m_typeList, &QListWidget::itemChanged, this, &PdbImportDialog::updateSelectionCount);
|
||||
connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
QString PdbImportDialog::pdbPath() const {
|
||||
return m_pathEdit->text();
|
||||
}
|
||||
|
||||
QVector<uint32_t> PdbImportDialog::selectedTypeIndices() const {
|
||||
QVector<uint32_t> result;
|
||||
for (int i = 0; i < m_typeList->count(); i++) {
|
||||
auto* item = m_typeList->item(i);
|
||||
if (item->checkState() == Qt::Checked) {
|
||||
uint32_t typeIndex = item->data(Qt::UserRole).toUInt();
|
||||
result.append(typeIndex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PdbImportDialog::browsePdb() {
|
||||
QString path = QFileDialog::getOpenFileName(this,
|
||||
"Select PDB File", {},
|
||||
"PDB Files (*.pdb);;All Files (*)");
|
||||
if (path.isEmpty()) return;
|
||||
m_pathEdit->setText(path);
|
||||
loadPdb();
|
||||
}
|
||||
|
||||
void PdbImportDialog::loadPdb() {
|
||||
QString path = m_pathEdit->text();
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
m_typeList->clear();
|
||||
m_allTypes.clear();
|
||||
m_countLabel->setText("Loading...");
|
||||
m_typeList->setEnabled(false);
|
||||
m_filterEdit->setEnabled(false);
|
||||
m_selectAll->setEnabled(false);
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
QApplication::processEvents();
|
||||
|
||||
QString error;
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(path, &error);
|
||||
|
||||
if (types.isEmpty()) {
|
||||
m_countLabel->setText(error.isEmpty() ? "No types found" : error);
|
||||
return;
|
||||
}
|
||||
|
||||
m_allTypes.reserve(types.size());
|
||||
for (const auto& t : types) {
|
||||
TypeItem item;
|
||||
item.typeIndex = t.typeIndex;
|
||||
item.name = t.name;
|
||||
item.childCount = t.childCount;
|
||||
item.isUnion = t.isUnion;
|
||||
m_allTypes.append(item);
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
std::sort(m_allTypes.begin(), m_allTypes.end(),
|
||||
[](const TypeItem& a, const TypeItem& b) { return a.name < b.name; });
|
||||
|
||||
m_filterEdit->setEnabled(true);
|
||||
m_selectAll->setEnabled(true);
|
||||
m_typeList->setEnabled(true);
|
||||
populateList();
|
||||
}
|
||||
|
||||
void PdbImportDialog::populateList() {
|
||||
m_typeList->blockSignals(true);
|
||||
m_typeList->clear();
|
||||
|
||||
QString filter = m_filterEdit->text();
|
||||
bool selectAll = m_selectAll->isChecked();
|
||||
|
||||
for (const auto& t : m_allTypes) {
|
||||
if (!filter.isEmpty() && !t.name.contains(filter, Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
QString label = QStringLiteral("%1 (%2 fields)")
|
||||
.arg(t.name).arg(t.childCount);
|
||||
auto* item = new QListWidgetItem(label, m_typeList);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(selectAll ? Qt::Checked : Qt::Unchecked);
|
||||
item->setData(Qt::UserRole, t.typeIndex);
|
||||
}
|
||||
|
||||
m_typeList->blockSignals(false);
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
void PdbImportDialog::filterChanged(const QString&) {
|
||||
populateList();
|
||||
}
|
||||
|
||||
void PdbImportDialog::selectAllToggled(bool) {
|
||||
populateList();
|
||||
}
|
||||
|
||||
void PdbImportDialog::updateSelectionCount() {
|
||||
int checked = 0;
|
||||
int total = m_typeList->count();
|
||||
for (int i = 0; i < total; i++) {
|
||||
if (m_typeList->item(i)->checkState() == Qt::Checked)
|
||||
checked++;
|
||||
}
|
||||
m_countLabel->setText(QStringLiteral("%1 of %2 types selected")
|
||||
.arg(checked).arg(m_allTypes.size()));
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(checked > 0);
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
53
src/imports/import_pdb_dialog.h
Normal file
53
src/imports/import_pdb_dialog.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QVector>
|
||||
#include <cstdint>
|
||||
|
||||
class QLineEdit;
|
||||
class QCheckBox;
|
||||
class QListWidget;
|
||||
class QLabel;
|
||||
class QDialogButtonBox;
|
||||
class QPushButton;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct PdbTypeInfo;
|
||||
|
||||
class PdbImportDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PdbImportDialog(QWidget* parent = nullptr);
|
||||
|
||||
QString pdbPath() const;
|
||||
QVector<uint32_t> selectedTypeIndices() const;
|
||||
|
||||
private slots:
|
||||
void browsePdb();
|
||||
void loadPdb();
|
||||
void filterChanged(const QString& text);
|
||||
void selectAllToggled(bool checked);
|
||||
void updateSelectionCount();
|
||||
|
||||
private:
|
||||
QLineEdit* m_pathEdit;
|
||||
QPushButton* m_browseBtn;
|
||||
QLineEdit* m_filterEdit;
|
||||
QCheckBox* m_selectAll;
|
||||
QListWidget* m_typeList;
|
||||
QLabel* m_countLabel;
|
||||
QDialogButtonBox* m_buttons;
|
||||
|
||||
struct TypeItem {
|
||||
uint32_t typeIndex;
|
||||
QString name;
|
||||
int childCount;
|
||||
bool isUnion;
|
||||
};
|
||||
QVector<TypeItem> m_allTypes;
|
||||
|
||||
void populateList();
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
387
src/imports/import_reclass_xml.cpp
Normal file
387
src/imports/import_reclass_xml.cpp
Normal file
@@ -0,0 +1,387 @@
|
||||
#include "import_reclass_xml.h"
|
||||
#include <QFile>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
#include <QDebug>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Version-specific type maps ──
|
||||
// Maps XML Type attribute (integer) → NodeKind.
|
||||
// Entries with no rcx equivalent use Hex8 as fallback.
|
||||
|
||||
enum class XmlVersion { V2013, V2016 };
|
||||
|
||||
// 2016 / ReClassEx / MemeClsEx type map (35 entries, index = XML Type value)
|
||||
static const struct { int xmlType; NodeKind kind; } kTypeMap2016[] = {
|
||||
// 0: null (unused)
|
||||
{ 1, NodeKind::Struct }, // ClassInstance
|
||||
// 2,3: null
|
||||
{ 4, NodeKind::Hex32 },
|
||||
{ 5, NodeKind::Hex64 },
|
||||
{ 6, NodeKind::Hex16 },
|
||||
{ 7, NodeKind::Hex8 },
|
||||
{ 8, NodeKind::Pointer64 }, // ClassPointer
|
||||
{ 9, NodeKind::Int64 },
|
||||
{ 10, NodeKind::Int32 },
|
||||
{ 11, NodeKind::Int16 },
|
||||
{ 12, NodeKind::Int8 },
|
||||
{ 13, NodeKind::Float },
|
||||
{ 14, NodeKind::Double },
|
||||
{ 15, NodeKind::UInt32 },
|
||||
{ 16, NodeKind::UInt16 },
|
||||
{ 17, NodeKind::UInt8 },
|
||||
{ 18, NodeKind::UTF8 }, // UTF8Text
|
||||
{ 19, NodeKind::UTF16 }, // UTF16Text
|
||||
{ 20, NodeKind::Pointer64 }, // FunctionPtr
|
||||
{ 21, NodeKind::Hex8 }, // Custom (expanded by Size)
|
||||
{ 22, NodeKind::Vec2 },
|
||||
{ 23, NodeKind::Vec3 },
|
||||
{ 24, NodeKind::Vec4 },
|
||||
{ 25, NodeKind::Mat4x4 },
|
||||
{ 26, NodeKind::Pointer64 }, // VTable
|
||||
{ 27, NodeKind::Array }, // ClassInstanceArray
|
||||
// 28: null (used for Class elements, not nodes)
|
||||
{ 29, NodeKind::Pointer64 }, // UTF8TextPtr
|
||||
{ 30, NodeKind::Pointer64 }, // UTF16TextPtr
|
||||
// 31: BitField → UInt8 fallback
|
||||
{ 31, NodeKind::UInt8 },
|
||||
{ 32, NodeKind::UInt64 },
|
||||
{ 33, NodeKind::Pointer64 }, // Function
|
||||
};
|
||||
|
||||
// 2013 / ReClass 2011 type map (31 entries)
|
||||
static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
|
||||
{ 1, NodeKind::Struct }, // ClassInstance
|
||||
{ 4, NodeKind::Hex32 },
|
||||
{ 5, NodeKind::Hex16 },
|
||||
{ 6, NodeKind::Hex8 },
|
||||
{ 7, NodeKind::Pointer64 }, // ClassPointer
|
||||
{ 8, NodeKind::Int32 },
|
||||
{ 9, NodeKind::Int16 },
|
||||
{ 10, NodeKind::Int8 },
|
||||
{ 11, NodeKind::Float },
|
||||
{ 12, NodeKind::UInt32 },
|
||||
{ 13, NodeKind::UInt16 },
|
||||
{ 14, NodeKind::UInt8 },
|
||||
{ 15, NodeKind::UTF8 }, // UTF8Text
|
||||
{ 16, NodeKind::Pointer64 }, // FunctionPtr
|
||||
{ 17, NodeKind::Hex8 }, // Custom
|
||||
{ 18, NodeKind::Vec2 },
|
||||
{ 19, NodeKind::Vec3 },
|
||||
{ 20, NodeKind::Vec4 },
|
||||
{ 21, NodeKind::Mat4x4 },
|
||||
{ 22, NodeKind::Pointer64 }, // VTable
|
||||
{ 23, NodeKind::Array }, // ClassInstanceArray
|
||||
{ 27, NodeKind::Int64 },
|
||||
{ 28, NodeKind::Double },
|
||||
{ 29, NodeKind::UTF16 }, // UTF16Text
|
||||
{ 30, NodeKind::Array }, // ClassPointerArray
|
||||
};
|
||||
|
||||
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) {
|
||||
for (const auto& e : kTypeMap2016)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
} else {
|
||||
for (const auto& e : kTypeMap2013)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
}
|
||||
return NodeKind::Hex8; // fallback
|
||||
}
|
||||
|
||||
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
|
||||
static bool isPointerType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016)
|
||||
return xmlType == 8 || xmlType == 20 || xmlType == 26 || xmlType == 29 || xmlType == 30 || xmlType == 33;
|
||||
else
|
||||
return xmlType == 7 || xmlType == 16 || xmlType == 22;
|
||||
}
|
||||
|
||||
// Is this XML type a ClassInstance (embedded struct)?
|
||||
static bool isClassInstanceType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 1;
|
||||
else return xmlType == 1;
|
||||
}
|
||||
|
||||
// Is this XML type a ClassInstanceArray?
|
||||
static bool isClassInstanceArrayType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 27;
|
||||
else return xmlType == 23 || xmlType == 30;
|
||||
}
|
||||
|
||||
// Is this XML type a text node?
|
||||
static bool isTextType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 18 || xmlType == 19;
|
||||
else return xmlType == 15 || xmlType == 29;
|
||||
}
|
||||
|
||||
// Is this XML type a UTF16 text node?
|
||||
static bool isUtf16TextType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 19;
|
||||
else return xmlType == 29;
|
||||
}
|
||||
|
||||
// Is this XML type a Custom node (expanded to hex)?
|
||||
static bool isCustomType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 21;
|
||||
else return xmlType == 17;
|
||||
}
|
||||
|
||||
// Deferred pointer resolution entry
|
||||
struct PendingRef {
|
||||
uint64_t nodeId;
|
||||
QString className;
|
||||
};
|
||||
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
qDebug() << "[ImportXML] Opening file:" << filePath;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qDebug() << "[ImportXML] ERROR: Cannot open file";
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file: ") + filePath;
|
||||
return {};
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] File size:" << file.size() << "bytes";
|
||||
|
||||
QXmlStreamReader xml(&file);
|
||||
XmlVersion version = XmlVersion::V2016; // default to 2016 (most common)
|
||||
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
|
||||
// Class name → struct node ID (for pointer resolution)
|
||||
QHash<QString, uint64_t> classIds;
|
||||
// Deferred pointer refs to resolve after all classes are parsed
|
||||
QVector<PendingRef> pendingRefs;
|
||||
|
||||
// Detect version from first comment
|
||||
bool versionDetected = false;
|
||||
|
||||
while (!xml.atEnd()) {
|
||||
xml.readNext();
|
||||
|
||||
// Detect version from XML comments
|
||||
if (!versionDetected && xml.isComment()) {
|
||||
QString comment = xml.text().toString().trimmed();
|
||||
if (comment.contains(QStringLiteral("ReClassEx"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("MemeClsEx"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("2016"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("2015"), Qt::CaseInsensitive)) {
|
||||
version = XmlVersion::V2016;
|
||||
} else if (comment.contains(QStringLiteral("2013"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("2011"), Qt::CaseInsensitive)) {
|
||||
version = XmlVersion::V2013;
|
||||
}
|
||||
// else keep default V2016
|
||||
versionDetected = true;
|
||||
qDebug() << "[ImportXML] Detected version:" << (version == XmlVersion::V2016 ? "V2016" : "V2013");
|
||||
}
|
||||
|
||||
if (!xml.isStartElement()) continue;
|
||||
|
||||
if (xml.name() == QStringLiteral("Class")) {
|
||||
// Parse a class element into a root Struct node
|
||||
QString className = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||
QString strOffset = xml.attributes().value(QStringLiteral("strOffset")).toString();
|
||||
|
||||
// Create root struct node (collapsed by default for large files)
|
||||
Node structNode;
|
||||
structNode.kind = NodeKind::Struct;
|
||||
structNode.name = className;
|
||||
structNode.structTypeName = className;
|
||||
structNode.parentId = 0; // root level
|
||||
structNode.offset = 0;
|
||||
structNode.collapsed = true;
|
||||
|
||||
int structIdx = tree.addNode(structNode);
|
||||
uint64_t structId = tree.nodes[structIdx].id;
|
||||
classIds[className] = structId;
|
||||
qDebug() << "[ImportXML] Class:" << className << "id:" << structId;
|
||||
|
||||
// Parse child Node elements
|
||||
int childOffset = 0;
|
||||
while (!xml.atEnd()) {
|
||||
xml.readNext();
|
||||
|
||||
if (xml.isEndElement() && xml.name() == QStringLiteral("Class"))
|
||||
break;
|
||||
|
||||
if (!xml.isStartElement() || xml.name() != QStringLiteral("Node"))
|
||||
continue;
|
||||
|
||||
int xmlType = xml.attributes().value(QStringLiteral("Type")).toInt();
|
||||
QString nodeName = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||
int nodeSize = xml.attributes().value(QStringLiteral("Size")).toInt();
|
||||
QString ptrClass = xml.attributes().value(QStringLiteral("Pointer")).toString();
|
||||
QString instClass = xml.attributes().value(QStringLiteral("Instance")).toString();
|
||||
|
||||
qDebug() << "[ImportXML] Node:" << nodeName << "type:" << xmlType
|
||||
<< "size:" << nodeSize << "ptr:" << ptrClass << "inst:" << instClass;
|
||||
|
||||
// Handle Custom type: expand to appropriate hex nodes
|
||||
if (isCustomType(xmlType, version) && nodeSize > 0) {
|
||||
// Pick best-fit hex kind
|
||||
NodeKind hexKind;
|
||||
int hexSize;
|
||||
if (nodeSize >= 8 && nodeSize % 8 == 0) {
|
||||
hexKind = NodeKind::Hex64; hexSize = 8;
|
||||
} else if (nodeSize >= 4 && nodeSize % 4 == 0) {
|
||||
hexKind = NodeKind::Hex32; hexSize = 4;
|
||||
} else if (nodeSize >= 2 && nodeSize % 2 == 0) {
|
||||
hexKind = NodeKind::Hex16; hexSize = 2;
|
||||
} else {
|
||||
hexKind = NodeKind::Hex8; hexSize = 1;
|
||||
}
|
||||
int count = nodeSize / hexSize;
|
||||
for (int i = 0; i < count; i++) {
|
||||
Node n;
|
||||
n.kind = hexKind;
|
||||
n.name = (count == 1) ? nodeName : QString();
|
||||
n.parentId = structId;
|
||||
n.offset = childOffset;
|
||||
tree.addNode(n);
|
||||
childOffset += hexSize;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeKind kind = lookupKind(xmlType, version);
|
||||
|
||||
// Handle ClassInstanceArray: read child <Array> element
|
||||
if (isClassInstanceArrayType(xmlType, version)) {
|
||||
qDebug() << "[ImportXML] -> ClassInstanceArray";
|
||||
int total = xml.attributes().value(QStringLiteral("Total")).toInt();
|
||||
if (total <= 0)
|
||||
total = xml.attributes().value(QStringLiteral("Count")).toInt();
|
||||
if (total <= 0) total = 1;
|
||||
|
||||
// Read child <Array> element for class name
|
||||
QString arrayClassName;
|
||||
while (!xml.atEnd()) {
|
||||
xml.readNext();
|
||||
if (xml.isEndElement() && xml.name() == QStringLiteral("Node"))
|
||||
break;
|
||||
if (xml.isStartElement() && xml.name() == QStringLiteral("Array")) {
|
||||
arrayClassName = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||
int arrayTotal = xml.attributes().value(QStringLiteral("Total")).toInt();
|
||||
if (arrayTotal <= 0)
|
||||
arrayTotal = xml.attributes().value(QStringLiteral("Count")).toInt();
|
||||
if (arrayTotal > 0) total = arrayTotal;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an Array node wrapping Struct elements
|
||||
Node arrNode;
|
||||
arrNode.kind = NodeKind::Array;
|
||||
arrNode.name = nodeName;
|
||||
arrNode.parentId = structId;
|
||||
arrNode.offset = childOffset;
|
||||
arrNode.arrayLen = total;
|
||||
arrNode.elementKind = NodeKind::Struct;
|
||||
if (!arrayClassName.isEmpty())
|
||||
arrNode.structTypeName = arrayClassName;
|
||||
int arrIdx = tree.addNode(arrNode);
|
||||
uint64_t arrId = tree.nodes[arrIdx].id;
|
||||
|
||||
// Defer ref resolution if array references a class
|
||||
if (!arrayClassName.isEmpty()) {
|
||||
pendingRefs.append({arrId, arrayClassName});
|
||||
}
|
||||
|
||||
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
Node n;
|
||||
n.kind = kind;
|
||||
n.name = nodeName;
|
||||
n.parentId = structId;
|
||||
n.offset = childOffset;
|
||||
|
||||
// Handle text nodes
|
||||
if (isTextType(xmlType, version)) {
|
||||
if (isUtf16TextType(xmlType, version))
|
||||
n.strLen = qMax(1, nodeSize / 2);
|
||||
else
|
||||
n.strLen = qMax(1, nodeSize);
|
||||
}
|
||||
|
||||
// Handle pointer types
|
||||
if (isPointerType(xmlType, version) && !ptrClass.isEmpty()) {
|
||||
qDebug() << "[ImportXML] -> Pointer to class:" << ptrClass;
|
||||
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});
|
||||
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle embedded class instance
|
||||
if (isClassInstanceType(xmlType, version)) {
|
||||
QString resolvedClass = instClass.isEmpty() ? ptrClass : instClass;
|
||||
qDebug() << "[ImportXML] -> ClassInstance:" << resolvedClass;
|
||||
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
|
||||
n.structTypeName = resolvedClass;
|
||||
if (!n.structTypeName.isEmpty()) {
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, n.structTypeName});
|
||||
} else {
|
||||
tree.addNode(n);
|
||||
}
|
||||
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
tree.addNode(n);
|
||||
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (xml.hasError() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
|
||||
qDebug() << "[ImportXML] XML parse error at line" << xml.lineNumber() << ":" << xml.errorString();
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("XML parse error at line %1: %2")
|
||||
.arg(xml.lineNumber())
|
||||
.arg(xml.errorString());
|
||||
return {};
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] Parsing complete. Total nodes:" << tree.nodes.size()
|
||||
<< "classes:" << classIds.size() << "pending refs:" << pendingRefs.size();
|
||||
|
||||
if (tree.nodes.isEmpty()) {
|
||||
qDebug() << "[ImportXML] ERROR: No classes found";
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No classes found in file");
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolve deferred pointer/struct references
|
||||
int resolved = 0, unresolved = 0;
|
||||
for (const auto& ref : pendingRefs) {
|
||||
int nodeIdx = tree.indexOfId(ref.nodeId);
|
||||
if (nodeIdx < 0) continue;
|
||||
|
||||
auto it = classIds.find(ref.className);
|
||||
if (it != classIds.end()) {
|
||||
tree.nodes[nodeIdx].refId = it.value();
|
||||
resolved++;
|
||||
} else {
|
||||
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
||||
unresolved++;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] Refs resolved:" << resolved << "unresolved:" << unresolved;
|
||||
qDebug() << "[ImportXML] Import complete. Returning tree with" << tree.nodes.size() << "nodes";
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
11
src/imports/import_reclass_xml.h
Normal file
11
src/imports/import_reclass_xml.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
|
||||
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
|
||||
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
1327
src/imports/import_source.cpp
Normal file
1327
src/imports/import_source.cpp
Normal file
File diff suppressed because it is too large
Load Diff
13
src/imports/import_source.h
Normal file
13
src/imports/import_source.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Import C/C++ struct definitions from source code into a NodeTree.
|
||||
// Supports two modes (auto-detected):
|
||||
// 1. With comment offsets (// 0xNN) - trusts the offset values
|
||||
// 2. Without comment offsets - computes offsets from type sizes
|
||||
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
1850
src/main.cpp
1850
src/main.cpp
File diff suppressed because it is too large
Load Diff
@@ -11,12 +11,18 @@
|
||||
#include <QDockWidget>
|
||||
#include <QTreeView>
|
||||
#include <QStandardItemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
#include <QButtonGroup>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class McpBridge;
|
||||
class ShimmerLabel;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -25,13 +31,14 @@ public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void newFile();
|
||||
void newDocument();
|
||||
void newClass();
|
||||
void newStruct();
|
||||
void newEnum();
|
||||
void selfTest();
|
||||
void openFile();
|
||||
void saveFile();
|
||||
void saveFileAs();
|
||||
|
||||
void closeFile();
|
||||
|
||||
void addNode();
|
||||
void removeNode();
|
||||
@@ -47,12 +54,22 @@ private slots:
|
||||
void toggleMcp();
|
||||
void setEditorFont(const QString& fontName);
|
||||
void exportCpp();
|
||||
void exportReclassXmlAction();
|
||||
void importFromSource();
|
||||
void importReclassXml();
|
||||
void importPdb();
|
||||
void showTypeAliasesDialog();
|
||||
void editTheme();
|
||||
void showOptionsDialog();
|
||||
|
||||
public:
|
||||
// Status bar helpers — separate app / MCP channels
|
||||
void setAppStatus(const QString& text);
|
||||
void setMcpStatus(const QString& text);
|
||||
void clearMcpStatus();
|
||||
|
||||
// Project Lifecycle API
|
||||
QMdiSubWindow* project_new();
|
||||
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);
|
||||
@@ -61,12 +78,19 @@ private:
|
||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||
|
||||
QMdiArea* m_mdiArea;
|
||||
QLabel* m_statusLabel;
|
||||
ShimmerLabel* m_statusLabel;
|
||||
QString m_appStatus;
|
||||
bool m_mcpBusy = false;
|
||||
QTimer* m_mcpClearTimer = nullptr;
|
||||
QButtonGroup* m_viewBtnGroup = nullptr;
|
||||
QPushButton* m_btnReclass = nullptr;
|
||||
QPushButton* m_btnRendered = nullptr;
|
||||
TitleBarWidget* m_titleBar = nullptr;
|
||||
QWidget* m_borderOverlay = nullptr;
|
||||
PluginManager m_pluginManager;
|
||||
McpBridge* m_mcp = nullptr;
|
||||
QAction* m_mcpAction = nullptr;
|
||||
QMenu* m_sourceMenu = nullptr;
|
||||
|
||||
struct SplitPane {
|
||||
QTabWidget* tabWidget = nullptr;
|
||||
@@ -84,11 +108,13 @@ private:
|
||||
int activePaneIdx = 0;
|
||||
};
|
||||
QMap<QMdiSubWindow*, TabState> m_tabs;
|
||||
|
||||
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
||||
void rebuildAllDocs();
|
||||
|
||||
void createMenus();
|
||||
void createStatusBar();
|
||||
void showPluginsDialog();
|
||||
void populateSourceMenu();
|
||||
QIcon makeIcon(const QString& svgPath);
|
||||
|
||||
RcxController* activeController() const;
|
||||
@@ -106,16 +132,20 @@ private:
|
||||
|
||||
SplitPane createSplitPane(TabState& tab);
|
||||
void applyTheme(const Theme& theme);
|
||||
void applyTabWidgetStyle(QTabWidget* tw);
|
||||
void styleTabCloseButtons();
|
||||
void syncViewButtons(ViewMode mode);
|
||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||
SplitPane* findActiveSplitPane();
|
||||
RcxEditor* activePaneEditor();
|
||||
|
||||
// Workspace dock
|
||||
QDockWidget* m_workspaceDock = nullptr;
|
||||
QTreeView* m_workspaceTree = nullptr;
|
||||
QStandardItemModel* m_workspaceModel = nullptr;
|
||||
QDockWidget* m_workspaceDock = nullptr;
|
||||
QTreeView* m_workspaceTree = nullptr;
|
||||
QStandardItemModel* m_workspaceModel = nullptr;
|
||||
QSortFilterProxyModel* m_workspaceProxy = nullptr;
|
||||
QLineEdit* m_workspaceSearch = nullptr;
|
||||
QLabel* m_dockTitleLabel = nullptr;
|
||||
QToolButton* m_dockCloseBtn = nullptr;
|
||||
void createWorkspaceDock();
|
||||
void rebuildWorkspaceModel();
|
||||
void updateBorderColor(const QColor& color);
|
||||
|
||||
@@ -170,9 +170,15 @@ void McpBridge::processLine(const QByteArray& line) {
|
||||
}
|
||||
|
||||
if (method == "initialize") {
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
|
||||
QCoreApplication::processEvents();
|
||||
sendJson(handleInitialize(id, req.value("params").toObject()));
|
||||
m_mainWindow->clearMcpStatus();
|
||||
} else if (method == "tools/list") {
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
|
||||
QCoreApplication::processEvents();
|
||||
sendJson(handleToolsList(id));
|
||||
m_mainWindow->clearMcpStatus();
|
||||
} else if (method == "tools/call") {
|
||||
sendJson(handleToolsCall(id, req.value("params").toObject()));
|
||||
} else {
|
||||
@@ -211,20 +217,29 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
// 1. project.state
|
||||
tools.append(QJsonObject{
|
||||
{"name", "project.state"},
|
||||
{"description", "Returns project state: node tree, base address, sources, provider info. "
|
||||
"Use depth/parentId to avoid dumping the whole tree. "
|
||||
"Call with depth:1 first to see top-level structs, then drill in with parentId."},
|
||||
{"description", "Returns project state with paginated node tree. "
|
||||
"Responses return max 'limit' nodes (default 50). "
|
||||
"Use depth:1 first, then parentId to drill into a struct. "
|
||||
"Enum/bitfield member arrays are omitted by default (counts shown instead); "
|
||||
"pass includeMembers:true to get full arrays. "
|
||||
"Response includes returned/total/nextOffset for paging."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
||||
{"depth", QJsonObject{{"type", "integer"},
|
||||
{"description", "Max tree depth to return (default 1 = top-level structs only)."}}},
|
||||
{"description", "Max tree depth to return (default 1)."}}},
|
||||
{"parentId", QJsonObject{{"type", "string"},
|
||||
{"description", "Only return children of this node."}}},
|
||||
{"includeTree", QJsonObject{{"type", "boolean"},
|
||||
{"description", "If false, return only provider/source info, no tree. Default true."}}}
|
||||
{"description", "If false, return only provider/source info, no tree. Default true."}}},
|
||||
{"includeMembers", QJsonObject{{"type", "boolean"},
|
||||
{"description", "If true, include full enumMembers/bitfieldMembers arrays. Default false (shows counts only)."}}},
|
||||
{"limit", QJsonObject{{"type", "integer"},
|
||||
{"description", "Max nodes to return (default 50, max 500)."}}},
|
||||
{"offset", QJsonObject{{"type", "integer"},
|
||||
{"description", "Skip this many nodes (for pagination). Use nextOffset from previous response."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
@@ -248,7 +263,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
|
||||
"Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. "
|
||||
"Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 "
|
||||
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Padding Struct Array"},
|
||||
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
@@ -287,7 +302,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
{"name", "hex.read"},
|
||||
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
|
||||
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
|
||||
"Offset is provider-relative (0-based) unless baseRelative=true."},
|
||||
"Offset is tree-relative (0-based, baseAddress added automatically) "
|
||||
"unless baseRelative=true (offset is absolute)."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
@@ -342,7 +358,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
{"description", "Trigger a UI action. Fallback for operations without dedicated tools. "
|
||||
"Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
|
||||
"export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, "
|
||||
"select_node, refresh"},
|
||||
"select_node, refresh. "
|
||||
"export_cpp accepts optional nodeId to export a single struct (recommended for large projects)."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
@@ -356,6 +373,28 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
}}
|
||||
});
|
||||
|
||||
// 8. tree.search
|
||||
tools.append(QJsonObject{
|
||||
{"name", "tree.search"},
|
||||
{"description", "Search for nodes by name (substring, case-insensitive). "
|
||||
"Returns compact results: id, name, kind, parentId, offset, childCount. "
|
||||
"Use kindFilter to narrow (e.g. 'Struct'). Max 100 results. "
|
||||
"Much faster than paging through project.state to find a specific type."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
||||
{"query", QJsonObject{{"type", "string"},
|
||||
{"description", "Name substring to search for (case-insensitive)."}}},
|
||||
{"kindFilter", QJsonObject{{"type", "string"},
|
||||
{"description", "Filter by node kind (e.g. 'Struct', 'Hex64', 'Array')."}}},
|
||||
{"limit", QJsonObject{{"type", "integer"},
|
||||
{"description", "Max results to return (default 20, max 100)."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
|
||||
return okReply(id, QJsonObject{{"tools", tools}});
|
||||
}
|
||||
|
||||
@@ -367,6 +406,10 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
QString toolName = params.value("name").toString();
|
||||
QJsonObject args = params.value("arguments").toObject();
|
||||
|
||||
// Show tool activity in status bar (with shimmer)
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
|
||||
QCoreApplication::processEvents(); // paint immediately
|
||||
|
||||
QJsonObject result;
|
||||
if (toolName == "project.state") result = toolProjectState(args);
|
||||
else if (toolName == "tree.apply") result = toolTreeApply(args);
|
||||
@@ -375,8 +418,11 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
else if (toolName == "hex.write") result = toolHexWrite(args);
|
||||
else if (toolName == "status.set") result = toolStatusSet(args);
|
||||
else if (toolName == "ui.action") result = toolUiAction(args);
|
||||
else if (toolName == "tree.search") result = toolTreeSearch(args);
|
||||
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||
|
||||
m_mainWindow->clearMcpStatus();
|
||||
|
||||
return okReply(id, result);
|
||||
}
|
||||
|
||||
@@ -435,6 +481,9 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
|
||||
int maxDepth = args.value("depth").toInt(1);
|
||||
bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true;
|
||||
bool includeMembers = args.value("includeMembers").toBool(false);
|
||||
int limit = qBound(1, args.value("limit").toInt(50), 500);
|
||||
int offset = qMax(0, args.value("offset").toInt(0));
|
||||
QString parentIdStr = args.value("parentId").toString();
|
||||
uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();
|
||||
|
||||
@@ -480,6 +529,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
state["modified"] = doc->modified;
|
||||
state["undoAvailable"] = doc->undoStack.canUndo();
|
||||
state["redoAvailable"] = doc->undoStack.canRedo();
|
||||
state["statusText"] = m_mainWindow->m_appStatus;
|
||||
|
||||
// Filtered tree: only emit nodes up to maxDepth from the filter root
|
||||
if (includeTree) {
|
||||
@@ -488,12 +538,15 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
// BFS from filterParentId, respecting maxDepth
|
||||
// BFS from filterParentId, respecting maxDepth + pagination
|
||||
QJsonArray nodeArr;
|
||||
struct QueueEntry { uint64_t parentId; int depth; };
|
||||
QVector<QueueEntry> queue;
|
||||
queue.append({filterParentId, 0});
|
||||
|
||||
int totalCount = 0; // total nodes that match depth filter
|
||||
int emitted = 0;
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
auto entry = queue.takeFirst();
|
||||
if (entry.depth > maxDepth) continue;
|
||||
@@ -501,13 +554,47 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
const auto& kids = childMap.value(entry.parentId);
|
||||
for (int ci : kids) {
|
||||
const Node& n = tree.nodes[ci];
|
||||
|
||||
// Count all matching nodes for pagination metadata
|
||||
totalCount++;
|
||||
|
||||
// Apply offset/limit pagination
|
||||
if (totalCount <= offset) {
|
||||
// Still skipping — but enqueue children for counting
|
||||
if (entry.depth + 1 <= maxDepth)
|
||||
queue.append({n.id, entry.depth + 1});
|
||||
continue;
|
||||
}
|
||||
if (emitted >= limit) {
|
||||
// Past limit — just keep counting total
|
||||
if (entry.depth + 1 <= maxDepth)
|
||||
queue.append({n.id, entry.depth + 1});
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject nj = n.toJson();
|
||||
|
||||
// Strip inline member arrays unless requested
|
||||
if (!includeMembers) {
|
||||
if (nj.contains("enumMembers")) {
|
||||
int count = nj.value("enumMembers").toArray().size();
|
||||
nj.remove("enumMembers");
|
||||
nj["enumMemberCount"] = count;
|
||||
}
|
||||
if (nj.contains("bitfieldMembers")) {
|
||||
int count = nj.value("bitfieldMembers").toArray().size();
|
||||
nj.remove("bitfieldMembers");
|
||||
nj["bitfieldMemberCount"] = count;
|
||||
}
|
||||
}
|
||||
|
||||
// Add computed size for containers
|
||||
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) {
|
||||
nj["computedSize"] = tree.structSpan(n.id, &childMap);
|
||||
nj["childCount"] = childMap.value(n.id).size();
|
||||
}
|
||||
nodeArr.append(nj);
|
||||
emitted++;
|
||||
|
||||
// Enqueue children if we haven't hit depth limit
|
||||
if (entry.depth + 1 <= maxDepth)
|
||||
@@ -519,6 +606,10 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
|
||||
treeObj["nextId"] = QString::number(tree.m_nextId);
|
||||
treeObj["nodes"] = nodeArr;
|
||||
treeObj["returned"] = emitted;
|
||||
treeObj["total"] = totalCount;
|
||||
if (emitted < totalCount)
|
||||
treeObj["nextOffset"] = offset + emitted;
|
||||
state["tree"] = treeObj;
|
||||
}
|
||||
|
||||
@@ -793,7 +884,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
||||
}
|
||||
|
||||
if (args.contains("pid")) {
|
||||
uint32_t pid = (uint32_t)args.value("pid").toInteger();
|
||||
uint32_t pid = (uint32_t)args.value("pid").toInt();
|
||||
QString name = args.value("processName").toString();
|
||||
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
||||
QString target = QString("%1:%2").arg(pid).arg(name);
|
||||
@@ -825,8 +916,8 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
|
||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||
int length = qMin(args.value("length").toInt(64), 4096);
|
||||
|
||||
if (args.value("baseRelative").toBool())
|
||||
offset -= (int64_t)tab->doc->tree.baseAddress;
|
||||
if (!args.value("baseRelative").toBool())
|
||||
offset += (int64_t)tab->doc->tree.baseAddress;
|
||||
|
||||
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
|
||||
return makeTextResult("Cannot read at offset " + QString::number(offset), true);
|
||||
@@ -907,8 +998,8 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
|
||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||
QString hexStr = args.value("hexBytes").toString().remove(' ');
|
||||
|
||||
if (args.value("baseRelative").toBool())
|
||||
offset -= (int64_t)doc->tree.baseAddress;
|
||||
if (!args.value("baseRelative").toBool())
|
||||
offset += (int64_t)doc->tree.baseAddress;
|
||||
|
||||
if (hexStr.size() % 2 != 0)
|
||||
return makeTextResult("Hex string must have even length", true);
|
||||
@@ -955,7 +1046,7 @@ QJsonObject McpBridge::toolStatusSet(const QJsonObject& args) {
|
||||
}
|
||||
}
|
||||
if (target == "statusBar" || target == "both") {
|
||||
m_mainWindow->m_statusLabel->setText(text);
|
||||
m_mainWindow->setAppStatus(text);
|
||||
}
|
||||
|
||||
return makeTextResult("Status set: " + text);
|
||||
@@ -1003,7 +1094,24 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
||||
if (action == "export_cpp") {
|
||||
if (!doc) return makeTextResult("No active tab", true);
|
||||
const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
|
||||
QString code = renderCppAll(doc->tree, aliases);
|
||||
QString code;
|
||||
if (!nodeIdStr.isEmpty()) {
|
||||
// Per-struct export
|
||||
uint64_t nid = nodeIdStr.toULongLong();
|
||||
code = renderCpp(doc->tree, nid, aliases);
|
||||
if (code.isEmpty())
|
||||
return makeTextResult("Node not found or not a struct: " + nodeIdStr, true);
|
||||
} else {
|
||||
code = renderCppAll(doc->tree, aliases);
|
||||
}
|
||||
// Truncate if too large (64 KB limit)
|
||||
if (code.size() > 65536) {
|
||||
int totalSize = code.size();
|
||||
code.truncate(65536);
|
||||
code += QStringLiteral("\n\n... truncated (%1 bytes total, showing first 64KB)"
|
||||
"\nUse nodeId param to export a single struct.")
|
||||
.arg(totalSize);
|
||||
}
|
||||
return makeTextResult(code);
|
||||
}
|
||||
if (action == "save_file") {
|
||||
@@ -1052,6 +1160,70 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
||||
return makeTextResult("Unknown action: " + action, true);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: tree.search
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab", true);
|
||||
|
||||
const auto& tree = tab->doc->tree;
|
||||
QString query = args.value("query").toString();
|
||||
QString kindFilter = args.value("kindFilter").toString();
|
||||
int limit = qBound(1, args.value("limit").toInt(20), 100);
|
||||
|
||||
if (query.isEmpty() && kindFilter.isEmpty())
|
||||
return makeTextResult("Provide 'query' (name substring) and/or 'kindFilter' (e.g. 'Struct')", true);
|
||||
|
||||
// Build parent→children map for childCount
|
||||
QHash<uint64_t, int> childCounts;
|
||||
for (const auto& n : tree.nodes)
|
||||
childCounts[n.parentId]++;
|
||||
|
||||
QJsonArray results;
|
||||
for (const auto& n : tree.nodes) {
|
||||
// Kind filter
|
||||
if (!kindFilter.isEmpty()) {
|
||||
if (kindToString(n.kind) != kindFilter) continue;
|
||||
}
|
||||
// Name substring match (case-insensitive)
|
||||
if (!query.isEmpty()) {
|
||||
bool nameMatch = n.name.contains(query, Qt::CaseInsensitive);
|
||||
bool typeMatch = n.structTypeName.contains(query, Qt::CaseInsensitive);
|
||||
if (!nameMatch && !typeMatch) continue;
|
||||
}
|
||||
|
||||
QJsonObject nj;
|
||||
nj["id"] = QString::number(n.id);
|
||||
nj["name"] = n.name;
|
||||
nj["kind"] = kindToString(n.kind);
|
||||
nj["parentId"] = QString::number(n.parentId);
|
||||
nj["offset"] = n.offset;
|
||||
if (!n.structTypeName.isEmpty())
|
||||
nj["structTypeName"] = n.structTypeName;
|
||||
if (!n.classKeyword.isEmpty())
|
||||
nj["classKeyword"] = n.classKeyword;
|
||||
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array)
|
||||
nj["childCount"] = childCounts.value(n.id, 0);
|
||||
if (!n.enumMembers.isEmpty())
|
||||
nj["enumMemberCount"] = n.enumMembers.size();
|
||||
if (!n.bitfieldMembers.isEmpty())
|
||||
nj["bitfieldMemberCount"] = n.bitfieldMembers.size();
|
||||
results.append(nj);
|
||||
|
||||
if (results.size() >= limit) break;
|
||||
}
|
||||
|
||||
QJsonObject out;
|
||||
out["results"] = results;
|
||||
out["count"] = results.size();
|
||||
out["query"] = query;
|
||||
if (!kindFilter.isEmpty()) out["kindFilter"] = kindFilter;
|
||||
return makeTextResult(QString::fromUtf8(
|
||||
QJsonDocument(out).toJson(QJsonDocument::Indented)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Notifications (call from MainWindow/Controller hooks)
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -58,6 +58,7 @@ private:
|
||||
QJsonObject toolHexWrite(const QJsonObject& args);
|
||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
|
||||
261
src/optionsdialog.cpp
Normal file
261
src/optionsdialog.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
#include "optionsdialog.h"
|
||||
#include "themes/thememanager.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("Options");
|
||||
setFixedSize(700, 450);
|
||||
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setSpacing(8);
|
||||
mainLayout->setContentsMargins(10, 10, 10, 10);
|
||||
|
||||
// -- Middle: left column (search + tree) | right column (pages) --
|
||||
auto* middleLayout = new QHBoxLayout;
|
||||
middleLayout->setSpacing(8);
|
||||
|
||||
// Left column: search bar + tree
|
||||
auto* leftColumn = new QVBoxLayout;
|
||||
leftColumn->setSpacing(4);
|
||||
|
||||
m_search = new QLineEdit;
|
||||
m_search->setPlaceholderText("Search Options (Ctrl+E)");
|
||||
m_search->setClearButtonEnabled(true);
|
||||
connect(m_search, &QLineEdit::textChanged, this, &OptionsDialog::filterTree);
|
||||
leftColumn->addWidget(m_search);
|
||||
|
||||
m_tree = new QTreeWidget;
|
||||
m_tree->setHeaderHidden(true);
|
||||
m_tree->setRootIsDecorated(true);
|
||||
m_tree->setFixedWidth(200);
|
||||
|
||||
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
|
||||
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
|
||||
m_tree->expandAll();
|
||||
m_tree->setCurrentItem(generalItem);
|
||||
leftColumn->addWidget(m_tree, 1);
|
||||
|
||||
middleLayout->addLayout(leftColumn);
|
||||
|
||||
// Right column: stacked pages with group boxes
|
||||
m_pages = new QStackedWidget;
|
||||
|
||||
// -- General page --
|
||||
auto* generalPage = new QWidget;
|
||||
auto* generalLayout = new QVBoxLayout(generalPage);
|
||||
generalLayout->setContentsMargins(0, 0, 0, 0);
|
||||
generalLayout->setSpacing(8);
|
||||
|
||||
// Refresh Rate group box
|
||||
auto* refreshGroup = new QGroupBox("Refresh Rate");
|
||||
auto* refreshLayout = new QFormLayout(refreshGroup);
|
||||
refreshLayout->setSpacing(8);
|
||||
refreshLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
m_refreshSpin = new QSpinBox;
|
||||
m_refreshSpin->setRange(1, 60000);
|
||||
m_refreshSpin->setSingleStep(50);
|
||||
m_refreshSpin->setValue(current.refreshMs);
|
||||
m_refreshSpin->setSuffix(" ms");
|
||||
m_refreshSpin->setObjectName("refreshSpin");
|
||||
refreshLayout->addRow("Interval:", m_refreshSpin);
|
||||
|
||||
auto* refreshDesc = new QLabel(
|
||||
"How often live memory is re-read and the view is updated, in milliseconds. "
|
||||
"Lower values give faster updates but use more CPU. Default: 660 ms.");
|
||||
refreshDesc->setWordWrap(true);
|
||||
refreshDesc->setContentsMargins(0, 0, 0, 0);
|
||||
refreshLayout->addRow(refreshDesc);
|
||||
|
||||
generalLayout->addWidget(refreshGroup);
|
||||
|
||||
// Visual Experience group box
|
||||
auto* visualGroup = new QGroupBox("Visual Experience");
|
||||
auto* visualLayout = new QFormLayout(visualGroup);
|
||||
visualLayout->setSpacing(8);
|
||||
visualLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
m_themeCombo = new QComboBox;
|
||||
auto& tm = ThemeManager::instance();
|
||||
for (const auto& theme : tm.themes())
|
||||
m_themeCombo->addItem(theme.name);
|
||||
m_themeCombo->setCurrentIndex(current.themeIndex);
|
||||
m_themeCombo->setObjectName("themeCombo");
|
||||
visualLayout->addRow("Color theme:", m_themeCombo);
|
||||
|
||||
m_fontCombo = new QComboBox;
|
||||
m_fontCombo->addItem("JetBrains Mono");
|
||||
m_fontCombo->addItem("Consolas");
|
||||
m_fontCombo->setCurrentText(current.fontName);
|
||||
m_fontCombo->setObjectName("fontCombo");
|
||||
visualLayout->addRow("Editor Font:", m_fontCombo);
|
||||
|
||||
m_titleCaseCheck = new QCheckBox("Apply title case styling to menu bar");
|
||||
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
|
||||
visualLayout->addRow(m_titleCaseCheck);
|
||||
|
||||
m_showIconCheck = new QCheckBox("Show icon in title bar");
|
||||
m_showIconCheck->setChecked(current.showIcon);
|
||||
visualLayout->addRow(m_showIconCheck);
|
||||
|
||||
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
|
||||
m_pageKeywords[generalItem] = collectPageKeywords(generalPage);
|
||||
|
||||
// -- AI Features page --
|
||||
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
|
||||
|
||||
auto* aiPage = new QWidget;
|
||||
auto* aiLayout = new QVBoxLayout(aiPage);
|
||||
aiLayout->setContentsMargins(0, 0, 0, 0);
|
||||
aiLayout->setSpacing(8);
|
||||
|
||||
auto* mcpGroup = new QGroupBox("MCP Server");
|
||||
auto* mcpLayout = new QVBoxLayout(mcpGroup);
|
||||
mcpLayout->setSpacing(4);
|
||||
|
||||
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
|
||||
m_autoMcpCheck->setChecked(current.autoStartMcp);
|
||||
mcpLayout->addWidget(m_autoMcpCheck);
|
||||
|
||||
auto* mcpDesc = new QLabel(
|
||||
"Automatically start the MCP bridge server when the application launches, "
|
||||
"allowing external AI tools to connect and interact with the editor.");
|
||||
mcpDesc->setWordWrap(true);
|
||||
mcpDesc->setContentsMargins(20, 0, 0, 0);
|
||||
mcpLayout->addWidget(mcpDesc);
|
||||
|
||||
aiLayout->addWidget(mcpGroup);
|
||||
aiLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(aiPage); // index 1
|
||||
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
|
||||
|
||||
// -- Generator page --
|
||||
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
|
||||
|
||||
auto* generatorPage = new QWidget;
|
||||
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
||||
generatorLayout->setContentsMargins(0, 0, 0, 0);
|
||||
generatorLayout->setSpacing(8);
|
||||
generatorLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(generatorPage); // index 2
|
||||
m_pageKeywords[generatorItem] = collectPageKeywords(generatorPage);
|
||||
|
||||
middleLayout->addWidget(m_pages, 1);
|
||||
|
||||
mainLayout->addLayout(middleLayout, 1);
|
||||
|
||||
// Tree <-> page connection
|
||||
m_itemPageIndex[generalItem] = 0;
|
||||
m_itemPageIndex[aiItem] = 1;
|
||||
m_itemPageIndex[generatorItem] = 2;
|
||||
connect(m_tree, &QTreeWidget::currentItemChanged, this,
|
||||
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
|
||||
if (!item) return;
|
||||
auto it = m_itemPageIndex.find(item);
|
||||
if (it != m_itemPageIndex.end())
|
||||
m_pages->setCurrentIndex(it.value());
|
||||
});
|
||||
|
||||
// -- Button box --
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(buttons);
|
||||
|
||||
}
|
||||
|
||||
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();
|
||||
return r;
|
||||
}
|
||||
|
||||
QStringList OptionsDialog::collectPageKeywords(QWidget* page) {
|
||||
QStringList keywords;
|
||||
for (auto* child : page->findChildren<QWidget*>()) {
|
||||
if (auto* label = qobject_cast<QLabel*>(child))
|
||||
keywords << label->text();
|
||||
else if (auto* cb = qobject_cast<QCheckBox*>(child))
|
||||
keywords << cb->text();
|
||||
else if (auto* gb = qobject_cast<QGroupBox*>(child))
|
||||
keywords << gb->title();
|
||||
else if (auto* combo = qobject_cast<QComboBox*>(child)) {
|
||||
for (int i = 0; i < combo->count(); ++i)
|
||||
keywords << combo->itemText(i);
|
||||
}
|
||||
}
|
||||
return keywords;
|
||||
}
|
||||
|
||||
void OptionsDialog::filterTree(const QString& text) {
|
||||
std::function<bool(QTreeWidgetItem*)> filter = [&](QTreeWidgetItem* item) -> bool {
|
||||
bool anyChildVisible = false;
|
||||
for (int i = 0; i < item->childCount(); ++i) {
|
||||
if (filter(item->child(i)))
|
||||
anyChildVisible = true;
|
||||
}
|
||||
|
||||
bool selfMatch = item->text(0).contains(text, Qt::CaseInsensitive);
|
||||
if (!selfMatch) {
|
||||
for (const auto& kw : m_pageKeywords.value(item)) {
|
||||
if (kw.contains(text, Qt::CaseInsensitive)) {
|
||||
selfMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool visible = selfMatch || anyChildVisible;
|
||||
item->setHidden(!visible);
|
||||
|
||||
if (visible && item->childCount() > 0)
|
||||
item->setExpanded(true);
|
||||
|
||||
return visible;
|
||||
};
|
||||
|
||||
for (int i = 0; i < m_tree->topLevelItemCount(); ++i)
|
||||
filter(m_tree->topLevelItem(i));
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
51
src/optionsdialog.h
Normal file
51
src/optionsdialog.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QTreeWidget>
|
||||
#include <QStackedWidget>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QHash>
|
||||
#include <QSpinBox>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct OptionsResult {
|
||||
int themeIndex = 0;
|
||||
QString fontName;
|
||||
bool menuBarTitleCase = true;
|
||||
bool showIcon = false;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = true;
|
||||
int refreshMs = 660;
|
||||
};
|
||||
|
||||
class OptionsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OptionsDialog(const OptionsResult& current, QWidget* parent = nullptr);
|
||||
|
||||
OptionsResult result() const;
|
||||
|
||||
private:
|
||||
void filterTree(const QString& text);
|
||||
static QStringList collectPageKeywords(QWidget* page);
|
||||
|
||||
QLineEdit* m_search = nullptr;
|
||||
QTreeWidget* m_tree = nullptr;
|
||||
QStackedWidget* m_pages = nullptr;
|
||||
QComboBox* m_themeCombo = nullptr;
|
||||
QComboBox* m_fontCombo = nullptr;
|
||||
QCheckBox* m_titleCaseCheck = nullptr;
|
||||
QCheckBox* m_showIconCheck = nullptr;
|
||||
QCheckBox* m_safeModeCheck = nullptr;
|
||||
QCheckBox* m_autoMcpCheck = nullptr;
|
||||
QSpinBox* m_refreshSpin = nullptr;
|
||||
|
||||
// searchable keywords per leaf tree item
|
||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||
// tree item → stacked widget page index
|
||||
QHash<QTreeWidgetItem*, int> m_itemPageIndex;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
@@ -92,7 +92,8 @@ bool PluginManager::LoadPlugin(const QString& path)
|
||||
IProviderPlugin* provider = static_cast<IProviderPlugin*>(plugin);
|
||||
QString name = QString::fromStdString(plugin->Name());
|
||||
QString identifier = name.toLower().replace(" ", "");
|
||||
ProviderRegistry::instance().registerProvider(name, identifier, provider);
|
||||
QString dllFileName = QFileInfo(path).fileName();
|
||||
ProviderRegistry::instance().registerProvider(name, identifier, provider, dllFileName);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -6,7 +6,8 @@ ProviderRegistry& ProviderRegistry::instance() {
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) {
|
||||
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier,
|
||||
IProviderPlugin* plugin, const QString& dllFileName) {
|
||||
// Check if already registered
|
||||
for (const auto& info : m_providers) {
|
||||
if (info.identifier == identifier) {
|
||||
@@ -14,8 +15,8 @@ void ProviderRegistry::registerProvider(const QString& name, const QString& iden
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_providers.append(ProviderInfo(name, identifier, plugin));
|
||||
|
||||
m_providers.append(ProviderInfo(name, identifier, plugin, dllFileName));
|
||||
qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")";
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,13 @@ public:
|
||||
IProviderPlugin* plugin; // Plugin (if plugin-based)
|
||||
BuiltinFactory factory; // Factory (if built-in)
|
||||
bool isBuiltin;
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p)
|
||||
: name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {}
|
||||
|
||||
QString dllFileName; // Original DLL/SO filename (plugin-based only)
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p,
|
||||
const QString& dll = {})
|
||||
: name(n), identifier(id), plugin(p), factory(nullptr),
|
||||
isBuiltin(false), dllFileName(dll) {}
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, BuiltinFactory f)
|
||||
: name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {}
|
||||
};
|
||||
@@ -36,7 +39,8 @@ public:
|
||||
static ProviderRegistry& instance();
|
||||
|
||||
// Register a plugin-based provider
|
||||
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin);
|
||||
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin,
|
||||
const QString& dllFileName = {});
|
||||
|
||||
// Register a built-in provider with a factory function
|
||||
void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory);
|
||||
|
||||
@@ -33,10 +33,10 @@ public:
|
||||
// Examples: "File", "Process", "Socket"
|
||||
virtual QString kind() const { return QStringLiteral("File"); }
|
||||
|
||||
// Base address for providers that offset reads (e.g. process memory).
|
||||
// Initial base address discovered by the provider (e.g. main module base).
|
||||
// Used by the controller to set tree.baseAddress on first attach.
|
||||
// For file/buffer providers this is always 0.
|
||||
virtual uint64_t base() const { return 0; }
|
||||
virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); }
|
||||
|
||||
// Resolve an absolute address to a symbol name.
|
||||
// Returns empty string if no symbol is known.
|
||||
@@ -47,6 +47,13 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolve a module/symbol name to its address (reverse of getSymbol).
|
||||
// Returns 0 if the name is not found.
|
||||
virtual uint64_t symbolToAddress(const QString& name) const {
|
||||
Q_UNUSED(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Derived convenience (non-virtual, never override) ---
|
||||
|
||||
bool isValid() const { return size() > 0; }
|
||||
|
||||
@@ -67,6 +67,9 @@ public:
|
||||
QString getSymbol(uint64_t addr) const override {
|
||||
return m_real ? m_real->getSymbol(addr) : QString();
|
||||
}
|
||||
uint64_t symbolToAddress(const QString& n) const override {
|
||||
return m_real ? m_real->symbolToAddress(n) : 0;
|
||||
}
|
||||
|
||||
bool write(uint64_t addr, const void* buf, int len) override {
|
||||
if (!m_real) return false;
|
||||
|
||||
@@ -47,5 +47,13 @@
|
||||
<file alias="selection.svg">vsicons/list-selection.svg</file>
|
||||
<file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file>
|
||||
<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="folder.svg">vsicons/folder.svg</file>
|
||||
<file alias="symbol-enum.svg">vsicons/symbol-enum.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>
|
||||
<file alias="clear-all.svg">vsicons/clear-all.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
32
src/themes/defaults/mid.json
Normal file
32
src/themes/defaults/mid.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Mid",
|
||||
"background": "#0D1219",
|
||||
"backgroundAlt": "#121720",
|
||||
"surface": "#161C28",
|
||||
"border": "#1E2636",
|
||||
"borderFocused": "#485068",
|
||||
"button": "#181E2C",
|
||||
"text": "#B0B8CC",
|
||||
"textDim": "#505C74",
|
||||
"textMuted": "#384258",
|
||||
"textFaint": "#2C3448",
|
||||
"hover": "#121720",
|
||||
"selected": "#121720",
|
||||
"selection": "#1A2038",
|
||||
"syntaxKeyword": "#5688C0",
|
||||
"syntaxNumber": "#90B480",
|
||||
"syntaxString": "#B88060",
|
||||
"syntaxComment": "#385030",
|
||||
"syntaxPreproc": "#9868A8",
|
||||
"syntaxType": "#8FDBFE",
|
||||
"indHoverSpan": "#C09038",
|
||||
"indCmdPill": "#141A26",
|
||||
"indDataChanged": "#608C54",
|
||||
"indHeatCold": "#B09030",
|
||||
"indHeatWarm": "#C09038",
|
||||
"indHeatHot": "#C83838",
|
||||
"indHintGreen": "#385830",
|
||||
"markerPtr": "#C83838",
|
||||
"markerCycle": "#B89028",
|
||||
"markerError": "#481818"
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#585858",
|
||||
"textFaint": "#505050",
|
||||
"hover": "#2b2b2b",
|
||||
"selected": "#232323",
|
||||
"hover": "#2a2a2a",
|
||||
"selected": "#2a2d2e",
|
||||
"selection": "#2b2b2b",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
@@ -22,6 +22,9 @@
|
||||
"indHoverSpan": "#E6B450",
|
||||
"indCmdPill": "#2a2a2a",
|
||||
"indDataChanged": "#8fbc7a",
|
||||
"indHeatCold": "#D4A945",
|
||||
"indHeatWarm": "#E6B450",
|
||||
"indHeatHot": "#f44747",
|
||||
"indHintGreen": "#5a8248",
|
||||
"markerPtr": "#f44747",
|
||||
"markerCycle": "#e5a00d",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#636369",
|
||||
"textFaint": "#4d4d55",
|
||||
"hover": "#3e3e42",
|
||||
"selected": "#2d2d30",
|
||||
"hover": "#2c2c2f",
|
||||
"selected": "#262629",
|
||||
"selection": "#264f78",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
@@ -22,6 +22,9 @@
|
||||
"indHoverSpan": "#b180d7",
|
||||
"indCmdPill": "#2d2d30",
|
||||
"indDataChanged": "#8fbc7a",
|
||||
"indHeatCold": "#D4A945",
|
||||
"indHeatWarm": "#d69d85",
|
||||
"indHeatHot": "#f44747",
|
||||
"indHintGreen": "#5a8248",
|
||||
"markerPtr": "#f44747",
|
||||
"markerCycle": "#e5a00d",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#7a7a6e",
|
||||
"textMuted": "#555550",
|
||||
"textFaint": "#464646",
|
||||
"hover": "#373737",
|
||||
"selected": "#2d2d2d",
|
||||
"hover": "#282828",
|
||||
"selected": "#262626",
|
||||
"selection": "#21213A",
|
||||
"syntaxKeyword": "#AA9565",
|
||||
"syntaxNumber": "#AAA98C",
|
||||
@@ -22,6 +22,9 @@
|
||||
"indHoverSpan": "#AA9565",
|
||||
"indCmdPill": "#2a2a2a",
|
||||
"indDataChanged": "#6B959F",
|
||||
"indHeatCold": "#C4A44A",
|
||||
"indHeatWarm": "#AA9565",
|
||||
"indHeatHot": "#A05040",
|
||||
"indHintGreen": "#464646",
|
||||
"markerPtr": "#6B3B21",
|
||||
"markerCycle": "#AA9565",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "theme.h"
|
||||
#include <QtGlobal>
|
||||
#include <type_traits>
|
||||
|
||||
namespace rcx {
|
||||
@@ -28,6 +29,9 @@ const ThemeFieldMeta kThemeFields[] = {
|
||||
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
|
||||
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
|
||||
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
|
||||
{"indHeatCold", "Heat Cold", "Indicators", &Theme::indHeatCold},
|
||||
{"indHeatWarm", "Heat Warm", "Indicators", &Theme::indHeatWarm},
|
||||
{"indHeatHot", "Heat Hot", "Indicators", &Theme::indHeatHot},
|
||||
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
|
||||
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
|
||||
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
|
||||
@@ -50,6 +54,23 @@ Theme Theme::fromJson(const QJsonObject& o) {
|
||||
if (o.contains(kThemeFields[i].key))
|
||||
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
||||
}
|
||||
// Derive heat colors from the theme's own palette when keys are absent
|
||||
// cold = muted yellow, warm = hover/string amber, hot = marker red
|
||||
if (!t.indHeatCold.isValid())
|
||||
t.indHeatCold = QColor("#D4A945");
|
||||
if (!t.indHeatWarm.isValid())
|
||||
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
|
||||
if (!t.indHeatHot.isValid())
|
||||
t.indHeatHot = t.markerPtr;
|
||||
|
||||
// Ensure hover is visually distinct from background
|
||||
if (t.hover.isValid() && t.background.isValid()) {
|
||||
int dist = qAbs(t.hover.red() - t.background.red())
|
||||
+ qAbs(t.hover.green() - t.background.green())
|
||||
+ qAbs(t.hover.blue() - t.background.blue());
|
||||
if (dist < 20)
|
||||
t.hover = t.background.lighter(130);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,10 @@ struct Theme {
|
||||
// ── Indicators ──
|
||||
QColor indHoverSpan; // hover link text
|
||||
QColor indCmdPill; // command row pill bg
|
||||
QColor indDataChanged; // changed data values
|
||||
QColor indDataChanged; // changed data values (legacy, fallback for old themes)
|
||||
QColor indHeatCold; // heatmap level 1 (changed once)
|
||||
QColor indHeatWarm; // heatmap level 2 (moderate changes)
|
||||
QColor indHeatHot; // heatmap level 3 (frequent changes)
|
||||
QColor indHintGreen; // comment/hint text
|
||||
|
||||
// ── Markers ──
|
||||
|
||||
@@ -18,7 +18,11 @@ ThemeManager::ThemeManager() {
|
||||
loadUserThemes();
|
||||
|
||||
QSettings settings("Reclass", "Reclass");
|
||||
QString fallback = m_builtIn.isEmpty() ? QString() : m_builtIn[0].name;
|
||||
QString fallback;
|
||||
for (const auto& t : m_builtIn) {
|
||||
if (t.name.contains("VS2022", Qt::CaseInsensitive)) { fallback = t.name; break; }
|
||||
}
|
||||
if (fallback.isEmpty() && !m_builtIn.isEmpty()) fallback = m_builtIn[0].name;
|
||||
QString saved = settings.value("theme", fallback).toString();
|
||||
auto all = themes();
|
||||
for (int i = 0; i < all.size(); i++) {
|
||||
|
||||
@@ -76,14 +76,16 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.textDim.name()));
|
||||
|
||||
// Menu bar styling — transparent background, themed text
|
||||
m_menuBar->setStyleSheet(
|
||||
QStringLiteral(
|
||||
"QMenuBar { background: transparent; border: none; }"
|
||||
"QMenuBar::item { background: transparent; color: %1; padding: 8px 8px 4px 8px; }"
|
||||
"QMenuBar::item:selected { background: %2; }"
|
||||
"QMenuBar::item:pressed { background: %2; }")
|
||||
.arg(theme.textDim.name(), theme.hover.name()));
|
||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
||||
{
|
||||
QPalette mbPal = m_menuBar->palette();
|
||||
mbPal.setColor(QPalette::Window, theme.background);
|
||||
mbPal.setColor(QPalette::Button, theme.background);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.textDim);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
}
|
||||
|
||||
// Chrome buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
@@ -114,6 +116,33 @@ void TitleBarWidget::setShowIcon(bool show) {
|
||||
}
|
||||
}
|
||||
|
||||
void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
||||
m_titleCase = titleCase;
|
||||
for (QAction* action : m_menuBar->actions()) {
|
||||
QString text = action->text();
|
||||
QString clean = text;
|
||||
clean.remove('&');
|
||||
|
||||
if (titleCase) {
|
||||
action->setText("&" + clean.toUpper());
|
||||
} else {
|
||||
QString result;
|
||||
bool capitalizeNext = true;
|
||||
for (int i = 0; i < clean.length(); ++i) {
|
||||
QChar ch = clean[i];
|
||||
if (ch.isLetter()) {
|
||||
result += capitalizeNext ? ch.toUpper() : ch.toLower();
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result += ch;
|
||||
if (ch.isSpace()) capitalizeNext = true;
|
||||
}
|
||||
}
|
||||
action->setText("&" + result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TitleBarWidget::updateMaximizeIcon() {
|
||||
if (window()->isMaximized())
|
||||
m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg"));
|
||||
|
||||
@@ -16,6 +16,8 @@ public:
|
||||
QMenuBar* menuBar() const { return m_menuBar; }
|
||||
void applyTheme(const Theme& theme);
|
||||
void setShowIcon(bool show);
|
||||
void setMenuBarTitleCase(bool titleCase);
|
||||
bool menuBarTitleCase() const { return m_titleCase; }
|
||||
|
||||
void updateMaximizeIcon();
|
||||
|
||||
@@ -32,6 +34,7 @@ private:
|
||||
QToolButton* m_btnClose = nullptr;
|
||||
|
||||
Theme m_theme;
|
||||
bool m_titleCase = false;
|
||||
|
||||
QToolButton* makeChromeButton(const QString& iconPath);
|
||||
void toggleMaximize();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
#include <QIntValidator>
|
||||
#include <QElapsedTimer>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
namespace rcx {
|
||||
@@ -31,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) {
|
||||
if (s.endsWith('*')) {
|
||||
spec.isPointer = true;
|
||||
s.chop(1);
|
||||
if (s.endsWith('*')) s.chop(1); // double pointer
|
||||
spec.ptrDepth = 1;
|
||||
if (s.endsWith('*')) { s.chop(1); spec.ptrDepth = 2; }
|
||||
spec.baseName = s.trimmed();
|
||||
return spec;
|
||||
}
|
||||
@@ -96,6 +98,12 @@ public:
|
||||
int h = option.rect.height();
|
||||
int w = option.rect.width();
|
||||
|
||||
// Scale metrics from font height
|
||||
QFontMetrics fmMain(m_font);
|
||||
int iconSz = fmMain.height(); // icon matches text height
|
||||
int gutterW = fmMain.horizontalAdvance(QChar(0x25B8)) + 4;
|
||||
int iconColW = iconSz + 4;
|
||||
|
||||
// Section: centered dim text with horizontal rules
|
||||
if (isSection) {
|
||||
painter->setPen(t.textDim);
|
||||
@@ -121,7 +129,7 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
// 18px gutter: side triangle if current
|
||||
// Gutter: side triangle if current
|
||||
if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) {
|
||||
const TypeEntry& entry = (*m_filtered)[row];
|
||||
bool isCurrent = false;
|
||||
@@ -130,20 +138,20 @@ public:
|
||||
else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
|
||||
isCurrent = (entry.structId == m_current->structId);
|
||||
if (isCurrent) {
|
||||
painter->setPen(t.syntaxType);
|
||||
painter->setPen(t.text);
|
||||
painter->setFont(m_font);
|
||||
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
||||
painter->drawText(QRect(x, y, gutterW, h), Qt::AlignCenter,
|
||||
QString(QChar(0x25B8)));
|
||||
}
|
||||
}
|
||||
x += 18;
|
||||
x += gutterW;
|
||||
|
||||
// Icon 16x16 — only for composite entries
|
||||
// Icon (scaled to font height) — only for composite entries
|
||||
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||
&& (*m_filtered)[row].entryKind == TypeEntry::Composite);
|
||||
if (hasIcon) {
|
||||
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
|
||||
QPixmap pm = structIcon.pixmap(16, 16);
|
||||
QPixmap pm = structIcon.pixmap(iconSz, iconSz);
|
||||
if (isDisabled) {
|
||||
// Paint dimmed
|
||||
QPixmap dimmed(pm.size());
|
||||
@@ -152,12 +160,12 @@ public:
|
||||
p.setOpacity(0.35);
|
||||
p.drawPixmap(0, 0, pm);
|
||||
p.end();
|
||||
painter->drawPixmap(x, y + (h - 16) / 2, dimmed);
|
||||
painter->drawPixmap(x, y + (h - iconSz) / 2, dimmed);
|
||||
} else {
|
||||
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
|
||||
structIcon.paint(painter, x, y + (h - iconSz) / 2, iconSz, iconSz);
|
||||
}
|
||||
}
|
||||
x += 20;
|
||||
x += iconColW;
|
||||
|
||||
// Text
|
||||
QColor textColor;
|
||||
@@ -272,14 +280,14 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
|
||||
// Separator
|
||||
{
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
m_separator = new QFrame;
|
||||
m_separator->setFrameShape(QFrame::HLine);
|
||||
m_separator->setFrameShadow(QFrame::Plain);
|
||||
QPalette sepPal = pal;
|
||||
sepPal.setColor(QPalette::WindowText, theme.border);
|
||||
sep->setPalette(sepPal);
|
||||
sep->setFixedHeight(1);
|
||||
layout->addWidget(sep);
|
||||
m_separator->setPalette(sepPal);
|
||||
m_separator->setFixedHeight(1);
|
||||
layout->addWidget(m_separator);
|
||||
}
|
||||
|
||||
// Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ]
|
||||
@@ -333,7 +341,12 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
this, [this](int id, bool checked) {
|
||||
if (!checked) return;
|
||||
m_arrayCountEdit->setVisible(id == 3);
|
||||
if (id == 3) m_arrayCountEdit->setFocus();
|
||||
if (id == 3) {
|
||||
if (m_arrayCountEdit->text().trimmed().isEmpty())
|
||||
m_arrayCountEdit->setText(QStringLiteral("1"));
|
||||
m_arrayCountEdit->setFocus();
|
||||
m_arrayCountEdit->selectAll();
|
||||
}
|
||||
updateModifierPreview();
|
||||
});
|
||||
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
||||
@@ -368,6 +381,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_listView->setFrameShape(QFrame::NoFrame);
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setMouseTracking(true);
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||
m_listView->installEventFilter(this);
|
||||
|
||||
@@ -384,10 +398,33 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::warmUp() {
|
||||
// One-time per-process cost (~170ms): Qt lazily initializes the style/font/DLL
|
||||
// subsystem the first time a popup with complex children is shown. Pre-pay it
|
||||
// by briefly showing a throwaway dummy popup with a QListView, then show+hide
|
||||
// ourselves.
|
||||
{
|
||||
auto* primer = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
|
||||
primer->resize(300, 400);
|
||||
auto* lay = new QVBoxLayout(primer);
|
||||
lay->addWidget(new QLabel(QStringLiteral("x")));
|
||||
lay->addWidget(new QLineEdit);
|
||||
auto* model = new QStringListModel(primer);
|
||||
QStringList items; for (int i = 0; i < 10; i++) items << QStringLiteral("x");
|
||||
model->setStringList(items);
|
||||
auto* lv = new QListView;
|
||||
lv->setModel(model);
|
||||
lay->addWidget(lv);
|
||||
primer->show();
|
||||
QApplication::processEvents();
|
||||
primer->hide();
|
||||
QApplication::processEvents();
|
||||
delete primer;
|
||||
}
|
||||
|
||||
TypeEntry dummy;
|
||||
dummy.entryKind = TypeEntry::Primitive;
|
||||
dummy.primitiveKind = NodeKind::Hex8;
|
||||
dummy.displayName = "warmup";
|
||||
dummy.displayName = QStringLiteral("warmup");
|
||||
setTypes({dummy});
|
||||
popup(QPoint(-9999, -9999));
|
||||
hide();
|
||||
@@ -419,28 +456,92 @@ void TypeSelectorPopup::setFont(const QFont& font) {
|
||||
delegate->setFont(font);
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::applyTheme(const Theme& theme) {
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
pal.setColor(QPalette::Base, theme.background);
|
||||
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.hover);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
setPalette(pal);
|
||||
|
||||
m_titleLabel->setPalette(pal);
|
||||
m_filterEdit->setPalette(pal);
|
||||
m_listView->setPalette(pal);
|
||||
m_previewLabel->setPalette(pal);
|
||||
m_arrayCountEdit->setPalette(pal);
|
||||
|
||||
// Separator
|
||||
QPalette sepPal = pal;
|
||||
sepPal.setColor(QPalette::WindowText, theme.border);
|
||||
m_separator->setPalette(sepPal);
|
||||
|
||||
// Esc button
|
||||
m_escLabel->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 2px 6px; }"
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
|
||||
// Create button
|
||||
m_createBtn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 3px 6px; }"
|
||||
"QToolButton:hover { color: %2; background: %3; }")
|
||||
.arg(theme.textMuted.name(), theme.text.name(), theme.hover.name()));
|
||||
|
||||
// Modifier toggle buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
|
||||
" padding: 2px 8px; border-radius: 3px; }"
|
||||
"QToolButton:checked { color: %4; background: %5; border-color: %5; }"
|
||||
"QToolButton:hover:!checked { background: %6; }")
|
||||
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
|
||||
theme.text.name(), theme.selected.name(), theme.hover.name());
|
||||
m_btnPlain->setStyleSheet(btnStyle);
|
||||
m_btnPtr->setStyleSheet(btnStyle);
|
||||
m_btnDblPtr->setStyleSheet(btnStyle);
|
||||
m_btnArray->setStyleSheet(btnStyle);
|
||||
|
||||
// Preview label
|
||||
m_previewLabel->setStyleSheet(QStringLiteral(
|
||||
"QLabel { color: %1; padding: 1px 6px; }").arg(theme.syntaxType.name()));
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setTitle(const QString& title) {
|
||||
m_titleLabel->setText(title);
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setMode(TypePopupMode mode) {
|
||||
m_mode = mode;
|
||||
// Show modifier toggles for modes where type modifiers make sense
|
||||
bool showMods = (mode == TypePopupMode::FieldType
|
||||
|| mode == TypePopupMode::ArrayElement);
|
||||
m_modRow->setVisible(showMods);
|
||||
// Reset to plain when showing
|
||||
if (showMods) {
|
||||
m_btnPlain->setChecked(true);
|
||||
m_arrayCountEdit->clear();
|
||||
m_arrayCountEdit->hide();
|
||||
}
|
||||
// Always reset to plain — prevents stale state from leaking across modes
|
||||
// (PointerTarget hides buttons but applyFilter still reads their state)
|
||||
m_btnPlain->setChecked(true);
|
||||
m_arrayCountEdit->clear();
|
||||
m_arrayCountEdit->hide();
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
|
||||
m_currentNodeSize = bytes;
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setModifier(int modId, int arrayCount) {
|
||||
if (modId == 1) m_btnPtr->setChecked(true);
|
||||
else if (modId == 2) m_btnDblPtr->setChecked(true);
|
||||
else if (modId == 3) {
|
||||
m_btnArray->setChecked(true);
|
||||
m_arrayCountEdit->setText(QString::number(arrayCount));
|
||||
m_arrayCountEdit->show();
|
||||
} else {
|
||||
m_btnPlain->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
|
||||
m_allTypes = types;
|
||||
if (current) {
|
||||
@@ -450,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
m_currentEntry = TypeEntry{};
|
||||
m_hasCurrent = false;
|
||||
}
|
||||
// Reset modifier toggles
|
||||
m_btnPlain->setChecked(true);
|
||||
m_arrayCountEdit->clear();
|
||||
m_arrayCountEdit->hide();
|
||||
// Don't reset modifier buttons here — setMode() already resets to plain,
|
||||
// and setModifier() may have preselected a button between setMode/setTypes.
|
||||
m_previewLabel->hide();
|
||||
|
||||
m_filterEdit->clear();
|
||||
@@ -467,7 +566,9 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||
QString text = t.classKeyword.isEmpty()
|
||||
? t.displayName
|
||||
: (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
||||
int w = 18 + 20 + fm.horizontalAdvance(text) + 16;
|
||||
int gutterW = fm.horizontalAdvance(QChar(0x25B8)) + 4;
|
||||
int iconColW = fm.height() + 4;
|
||||
int w = gutterW + iconColW + fm.horizontalAdvance(text) + 16;
|
||||
if (w > maxTextW) maxTextW = w;
|
||||
}
|
||||
int popupW = qBound(280, maxTextW + 24, 500);
|
||||
@@ -537,10 +638,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
|
||||
QString filterBase = text.trimmed();
|
||||
|
||||
// Separate primitives and composites
|
||||
// Separate primitives and composites (all types shown regardless of modifier)
|
||||
QVector<TypeEntry> primitives, composites;
|
||||
for (const auto& t : m_allTypes) {
|
||||
if (t.entryKind == TypeEntry::Section) continue; // skip stale sections
|
||||
if (t.entryKind == TypeEntry::Section) continue;
|
||||
bool matchesFilter = filterBase.isEmpty()
|
||||
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|
||||
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
||||
@@ -552,7 +653,11 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
composites.append(t);
|
||||
}
|
||||
|
||||
// For non-Root modes, sort primitives: same-size first, then rest
|
||||
auto alphabetical = [](const TypeEntry& a, const TypeEntry& b) {
|
||||
return a.displayName.compare(b.displayName, Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
|
||||
// For non-Root modes, sort primitives: same-size first, then rest — alphabetical within each group
|
||||
if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) {
|
||||
QVector<TypeEntry> sameSize, other;
|
||||
for (const auto& p : primitives) {
|
||||
@@ -561,7 +666,11 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
else
|
||||
other.append(p);
|
||||
}
|
||||
std::sort(sameSize.begin(), sameSize.end(), alphabetical);
|
||||
std::sort(other.begin(), other.end(), alphabetical);
|
||||
primitives = sameSize + other;
|
||||
} else {
|
||||
std::sort(primitives.begin(), primitives.end(), alphabetical);
|
||||
}
|
||||
|
||||
// Helper lambdas for appending sections
|
||||
|
||||
@@ -16,6 +16,8 @@ class QWidget;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct Theme;
|
||||
|
||||
// ── Popup mode ──
|
||||
|
||||
enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget };
|
||||
@@ -38,6 +40,7 @@ struct TypeEntry {
|
||||
struct TypeSpec {
|
||||
QString baseName;
|
||||
bool isPointer = false;
|
||||
int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer)
|
||||
int arrayCount = 0; // 0 = not array
|
||||
};
|
||||
|
||||
@@ -53,7 +56,9 @@ public:
|
||||
void setFont(const QFont& font);
|
||||
void setTitle(const QString& title);
|
||||
void setMode(TypePopupMode mode);
|
||||
void applyTheme(const Theme& theme);
|
||||
void setCurrentNodeSize(int bytes);
|
||||
void setModifier(int modId, int arrayCount = 0);
|
||||
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
|
||||
void popup(const QPoint& globalPos);
|
||||
|
||||
@@ -77,6 +82,7 @@ private:
|
||||
QLabel* m_previewLabel = nullptr;
|
||||
QListView* m_listView = nullptr;
|
||||
QStringListModel* m_model = nullptr;
|
||||
QFrame* m_separator = nullptr;
|
||||
|
||||
// Modifier toggles
|
||||
QWidget* m_modRow = nullptr;
|
||||
|
||||
@@ -1,62 +1,118 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Recursively add children of parentId as tree items under parentItem.
|
||||
inline void addWorkspaceChildren(QStandardItem* parentItem,
|
||||
const NodeTree& tree,
|
||||
uint64_t parentId,
|
||||
void* subPtr) {
|
||||
QVector<int> children = tree.childrenOf(parentId);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
struct TabInfo {
|
||||
const NodeTree* tree;
|
||||
QString name;
|
||||
void* subPtr; // QMdiSubWindow* as void*
|
||||
};
|
||||
|
||||
for (int idx : children) {
|
||||
const Node& node = tree.nodes[idx];
|
||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
||||
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
|
||||
|
||||
// Skip hex preview nodes — they are padding/filler, not meaningful fields
|
||||
if (isHexNode(node.kind)) continue;
|
||||
|
||||
QString display;
|
||||
if (node.kind == NodeKind::Struct) {
|
||||
QString typeName = node.structTypeName.isEmpty()
|
||||
? node.name : node.structTypeName;
|
||||
display = QStringLiteral("%1 (%2)")
|
||||
.arg(typeName, node.resolvedClassKeyword());
|
||||
} else {
|
||||
display = QStringLiteral("%1 (%2)")
|
||||
.arg(node.name, QString::fromLatin1(kindToString(node.kind)));
|
||||
}
|
||||
|
||||
auto* item = new QStandardItem(display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
if (node.kind == NodeKind::Struct)
|
||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 1);
|
||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 2); // nodeId for scroll
|
||||
|
||||
if (node.kind == NodeKind::Struct)
|
||||
addWorkspaceChildren(item, tree, node.id, subPtr);
|
||||
|
||||
parentItem->appendRow(item);
|
||||
}
|
||||
}
|
||||
|
||||
inline void buildWorkspaceModel(QStandardItemModel* model,
|
||||
const NodeTree& tree,
|
||||
const QString& projectName,
|
||||
void* subPtr = nullptr) {
|
||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
model->clear();
|
||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||
|
||||
auto* projectItem = new QStandardItem(projectName);
|
||||
projectItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
// 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);
|
||||
|
||||
addWorkspaceChildren(projectItem, tree, 0, subPtr);
|
||||
// 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) {
|
||||
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||
for (int idx : topLevel) {
|
||||
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});
|
||||
else
|
||||
types.append({&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;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
model->appendRow(projectItem);
|
||||
}
|
||||
|
||||
82
tests/bench_import_pdb.cpp
Normal file
82
tests/bench_import_pdb.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "imports/import_pdb.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class BenchImportPdb : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void benchEnumerateAll();
|
||||
void benchImportAll();
|
||||
};
|
||||
|
||||
static const QString kPdbPath = QStringLiteral(
|
||||
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
|
||||
|
||||
void BenchImportPdb::benchEnumerateAll() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
qDebug() << "enumeratePdbTypes:" << types.size() << "types in" << elapsed << "ms";
|
||||
}
|
||||
|
||||
void BenchImportPdb::benchImportAll() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
// Phase 1: enumerate
|
||||
QString err;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
qint64 enumerateMs = timer.elapsed();
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
|
||||
// Collect all type indices
|
||||
QVector<uint32_t> indices;
|
||||
indices.reserve(types.size());
|
||||
for (const auto& t : types)
|
||||
indices.append(t.typeIndex);
|
||||
|
||||
// Phase 2: import all
|
||||
timer.restart();
|
||||
int lastProgress = 0;
|
||||
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
|
||||
[&](int cur, int total) -> bool {
|
||||
// Report progress at 25% intervals
|
||||
int pct = (cur * 100) / total;
|
||||
if (pct >= lastProgress + 25) {
|
||||
qDebug() << " progress:" << cur << "/" << total
|
||||
<< "(" << pct << "%)";
|
||||
lastProgress = pct;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
qint64 importMs = timer.elapsed();
|
||||
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// Count root structs
|
||||
int rootCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== PDB Import Benchmark (ntkrnlmp.pdb) ===";
|
||||
qDebug() << " Enumerate:" << types.size() << "types in" << enumerateMs << "ms";
|
||||
qDebug() << " Import all:" << rootCount << "root structs,"
|
||||
<< tree.nodes.size() << "total nodes in" << importMs << "ms";
|
||||
qDebug() << " Total:" << (enumerateMs + importMs) << "ms";
|
||||
qDebug() << "============================================";
|
||||
}
|
||||
|
||||
QTEST_MAIN(BenchImportPdb)
|
||||
#include "bench_import_pdb.moc"
|
||||
219
tests/test_addressparser.cpp
Normal file
219
tests/test_addressparser.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "addressparser.h"
|
||||
#include <QTest>
|
||||
|
||||
using rcx::AddressParser;
|
||||
using rcx::AddressParserCallbacks;
|
||||
using rcx::AddressParseResult;
|
||||
|
||||
class TestAddressParser : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
// -- Hex literals --
|
||||
|
||||
void bareHex() { auto r = AddressParser::evaluate("AB"); QVERIFY(r.ok); QCOMPARE(r.value, 0xABULL); }
|
||||
void prefixedHex() { auto r = AddressParser::evaluate("0x1F4"); QVERIFY(r.ok); QCOMPARE(r.value, 0x1F4ULL); }
|
||||
void zeroLiteral() { auto r = AddressParser::evaluate("0"); QVERIFY(r.ok); QCOMPARE(r.value, 0ULL); }
|
||||
void large64bit() { auto r = AddressParser::evaluate("7FF66CCE0000");QVERIFY(r.ok); QCOMPARE(r.value, 0x7FF66CCE0000ULL); }
|
||||
|
||||
// -- Arithmetic --
|
||||
|
||||
void addition() {
|
||||
auto r = AddressParser::evaluate("0x100 + 0x200");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x300ULL);
|
||||
}
|
||||
void subtraction() {
|
||||
auto r = AddressParser::evaluate("0x300 - 0x100");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x200ULL);
|
||||
}
|
||||
void multiplication() {
|
||||
auto r = AddressParser::evaluate("0x10 * 4");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x40ULL);
|
||||
}
|
||||
void division() {
|
||||
auto r = AddressParser::evaluate("0x100 / 2");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x80ULL);
|
||||
}
|
||||
void precedence() {
|
||||
// 0x10 + 2*3 = 0x10 + 6 = 0x16
|
||||
auto r = AddressParser::evaluate("0x10 + 2 * 3");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x16ULL);
|
||||
}
|
||||
void parentheses() {
|
||||
// (0x10 + 2) * 3 = 0x12 * 3 = 0x36
|
||||
auto r = AddressParser::evaluate("(0x10 + 2) * 3");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x36ULL);
|
||||
}
|
||||
|
||||
// -- Unary minus --
|
||||
|
||||
void unaryMinus() {
|
||||
auto r = AddressParser::evaluate("-0x10 + 0x20");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x10ULL);
|
||||
}
|
||||
|
||||
// -- Module resolution --
|
||||
|
||||
void moduleResolve() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "Program.exe");
|
||||
return *ok ? 0x140000000ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("<Program.exe> + 0x123", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000123ULL);
|
||||
}
|
||||
|
||||
void moduleNotFound() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString&, bool* ok) -> uint64_t {
|
||||
*ok = false;
|
||||
return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("<NoSuch.dll>", 8, &cbs);
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("not found"));
|
||||
}
|
||||
|
||||
// -- Dereference --
|
||||
|
||||
void derefSimple() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
|
||||
*ok = (addr == 0x1000);
|
||||
return *ok ? 0xDEADBEEFULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xDEADBEEFULL);
|
||||
}
|
||||
|
||||
void derefNested() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "mod");
|
||||
return *ok ? 0x400000ULL : 0;
|
||||
};
|
||||
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
|
||||
*ok = true;
|
||||
if (addr == 0x400100) return 0x500000;
|
||||
if (addr == 0x900000) return 0xABCDEF;
|
||||
return 0;
|
||||
};
|
||||
// [<mod> + [<mod> + 0x100]] = [0x400000 + [0x400000+0x100]]
|
||||
// inner deref: [0x400100] = 0x500000
|
||||
// outer: [0x400000 + 0x500000] = [0x900000] = 0xABCDEF
|
||||
auto r = AddressParser::evaluate("[<mod> + [<mod> + 0x100]]", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xABCDEFULL);
|
||||
}
|
||||
|
||||
void derefReadFailure() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.readPointer = [](uint64_t, bool* ok) -> uint64_t {
|
||||
*ok = false;
|
||||
return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("failed to read"));
|
||||
}
|
||||
|
||||
// -- Complex expression from plan --
|
||||
|
||||
void complexExpr() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "Program.exe");
|
||||
return *ok ? 0x140000000ULL : 0;
|
||||
};
|
||||
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
|
||||
*ok = true;
|
||||
if (addr == 0x1400000DEULL) return 0x500000;
|
||||
return 0;
|
||||
};
|
||||
// [<Program.exe> + 0xDE] - AB = [0x1400000DE] - 0xAB = 0x500000 - 0xAB = 0x4FFF55
|
||||
auto r = AddressParser::evaluate("[<Program.exe> + 0xDE] - AB", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x4FFF55ULL);
|
||||
}
|
||||
|
||||
// -- Errors --
|
||||
|
||||
void emptyInput() {
|
||||
auto r = AddressParser::evaluate("");
|
||||
QVERIFY(!r.ok);
|
||||
}
|
||||
void unmatchedBracket() {
|
||||
auto r = AddressParser::evaluate("[0x1000");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("']'"));
|
||||
}
|
||||
void unmatchedAngle() {
|
||||
auto r = AddressParser::evaluate("<Program.exe");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("'>'"));
|
||||
}
|
||||
void divisionByZero() {
|
||||
auto r = AddressParser::evaluate("0x100 / 0");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("division by zero"));
|
||||
}
|
||||
void trailingGarbage() {
|
||||
auto r = AddressParser::evaluate("0x100 xyz");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("unexpected"));
|
||||
}
|
||||
void trailingOperator() {
|
||||
auto r = AddressParser::evaluate("0x100 +");
|
||||
QVERIFY(!r.ok);
|
||||
}
|
||||
|
||||
// -- Validation --
|
||||
|
||||
void validateValid() {
|
||||
QCOMPARE(AddressParser::validate("0x100 + 0x200"), QString());
|
||||
QCOMPARE(AddressParser::validate("<Prog.exe> + [0x100]"), QString());
|
||||
}
|
||||
void validateInvalid() {
|
||||
QVERIFY(!AddressParser::validate("").isEmpty());
|
||||
QVERIFY(!AddressParser::validate("[0x100").isEmpty());
|
||||
QVERIFY(!AddressParser::validate("0x100 xyz").isEmpty());
|
||||
}
|
||||
|
||||
// -- Backtick stripping --
|
||||
|
||||
void backtickStripping() {
|
||||
auto r = AddressParser::evaluate("7ff6`6cce0000");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x7FF66CCE0000ULL);
|
||||
}
|
||||
|
||||
// -- Whitespace tolerance --
|
||||
|
||||
void whitespace() {
|
||||
auto r = AddressParser::evaluate(" 0x100 + 0x200 ");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x300ULL);
|
||||
}
|
||||
|
||||
// -- Legacy compat: simple hex --
|
||||
|
||||
void simpleHexAddress() {
|
||||
auto r = AddressParser::evaluate("140000000");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000000ULL);
|
||||
}
|
||||
|
||||
// -- Multiple additions --
|
||||
|
||||
void multipleAdditions() {
|
||||
auto r = AddressParser::evaluate("0x100 + 0x200 + 0x300");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x600ULL);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestAddressParser)
|
||||
#include "test_addressparser.moc"
|
||||
@@ -1,185 +0,0 @@
|
||||
/**
|
||||
* test_com_security.cpp — DebugConnect transport diagnostic
|
||||
*
|
||||
* Tests EVERY transport to find what works from MinGW:
|
||||
* 1. TCP to WinDbg .server (port 5055)
|
||||
* 2. Named pipe to WinDbg .server
|
||||
* 3. TCP with various COM security configs
|
||||
* 4. DebugCreate local (baseline)
|
||||
*
|
||||
* SETUP: In WinDbg, run BOTH of these:
|
||||
* .server tcp:port=5055
|
||||
* .server npipe:pipe=reclass
|
||||
*
|
||||
* Then run this test.
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <objbase.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
static void try_connect(const char* label, const char* connStr)
|
||||
{
|
||||
printf(" %-40s → ", label);
|
||||
fflush(stdout);
|
||||
|
||||
IDebugClient* client = nullptr;
|
||||
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
|
||||
|
||||
if (SUCCEEDED(hr) && client) {
|
||||
printf("SUCCESS (hr=0x%08lX)\n", (unsigned long)hr);
|
||||
|
||||
// Try to get data spaces and read something
|
||||
IDebugDataSpaces* ds = nullptr;
|
||||
IDebugSymbols* sym = nullptr;
|
||||
IDebugControl* ctrl = nullptr;
|
||||
client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
|
||||
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||
|
||||
if (ctrl) {
|
||||
HRESULT hrWait = ctrl->WaitForEvent(0, 5000);
|
||||
printf(" WaitForEvent: hr=0x%08lX\n", (unsigned long)hrWait);
|
||||
}
|
||||
|
||||
if (sym) {
|
||||
ULONG numMods = 0, numUnloaded = 0;
|
||||
sym->GetNumberModules(&numMods, &numUnloaded);
|
||||
printf(" Modules: %lu loaded\n", numMods);
|
||||
|
||||
if (numMods > 0 && ds) {
|
||||
ULONG64 base = 0;
|
||||
sym->GetModuleByIndex(0, &base);
|
||||
unsigned char buf[2] = {};
|
||||
ULONG got = 0;
|
||||
ds->ReadVirtual(base, buf, 2, &got);
|
||||
printf(" Read at 0x%llX: got=%lu bytes=[%02X %02X]\n",
|
||||
(unsigned long long)base, got, buf[0], buf[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (sym) sym->Release();
|
||||
if (ds) ds->Release();
|
||||
if (ctrl) ctrl->Release();
|
||||
client->Release();
|
||||
} else {
|
||||
char buf[256] = {};
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, (DWORD)hr, 0, buf, sizeof(buf), nullptr);
|
||||
for (char* p = buf + strlen(buf) - 1; p >= buf && (*p == '\r' || *p == '\n'); --p)
|
||||
*p = '\0';
|
||||
printf("FAIL hr=0x%08lX (%s)\n", (unsigned long)hr, buf);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int main()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char hostname[256] = {};
|
||||
DWORD hsize = sizeof(hostname);
|
||||
GetComputerNameA(hostname, &hsize);
|
||||
|
||||
printf("=== DebugConnect Transport Diagnostic ===\n");
|
||||
printf("Machine: %s\n\n", hostname);
|
||||
|
||||
// ── Baseline: DebugCreate (local) ──
|
||||
printf("[1] DebugCreate (local, no network)\n");
|
||||
{
|
||||
IDebugClient* client = nullptr;
|
||||
HRESULT hr = DebugCreate(IID_IDebugClient, (void**)&client);
|
||||
printf(" DebugCreate: %s (hr=0x%08lX)\n\n",
|
||||
SUCCEEDED(hr) ? "OK" : "FAIL", (unsigned long)hr);
|
||||
if (client) client->Release();
|
||||
}
|
||||
|
||||
// ── TCP variants ──
|
||||
printf("[2] TCP connections (need: .server tcp:port=5055)\n");
|
||||
try_connect("tcp:Port=5055,Server=localhost",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
try_connect("tcp:Port=5055,Server=127.0.0.1",
|
||||
"tcp:Port=5055,Server=127.0.0.1");
|
||||
{
|
||||
char conn[512];
|
||||
snprintf(conn, sizeof(conn), "tcp:Port=5055,Server=%s", hostname);
|
||||
try_connect(conn, conn);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// ── Named pipe variants ──
|
||||
printf("[3] Named pipe connections (need: .server npipe:pipe=reclass)\n");
|
||||
try_connect("npipe:Pipe=reclass,Server=localhost",
|
||||
"npipe:Pipe=reclass,Server=localhost");
|
||||
{
|
||||
char conn[512];
|
||||
snprintf(conn, sizeof(conn), "npipe:Pipe=reclass,Server=%s", hostname);
|
||||
try_connect(conn, conn);
|
||||
}
|
||||
try_connect("npipe:Pipe=reclass",
|
||||
"npipe:Pipe=reclass");
|
||||
printf("\n");
|
||||
|
||||
// ── TCP with COM security ──
|
||||
printf("[4] TCP with explicit COM init (MTA + IMPERSONATE)\n");
|
||||
{
|
||||
// This runs in-process so CoInitialize affects subsequent calls
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
CoInitializeSecurity(
|
||||
nullptr, -1, nullptr, nullptr,
|
||||
RPC_C_AUTHN_LEVEL_DEFAULT,
|
||||
RPC_C_IMP_LEVEL_IMPERSONATE,
|
||||
nullptr, EOAC_NONE, nullptr);
|
||||
try_connect("tcp:Port=5055,Server=localhost (MTA+SEC)",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
try_connect("npipe:Pipe=reclass (MTA+SEC)",
|
||||
"npipe:Pipe=reclass,Server=localhost");
|
||||
CoUninitialize();
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// ── Check if dbgeng.dll is the system one ──
|
||||
printf("[5] DbgEng DLL info\n");
|
||||
{
|
||||
HMODULE hmod = GetModuleHandleA("dbgeng.dll");
|
||||
if (hmod) {
|
||||
char path[MAX_PATH] = {};
|
||||
GetModuleFileNameA(hmod, path, MAX_PATH);
|
||||
printf(" dbgeng.dll loaded from: %s\n", path);
|
||||
|
||||
// Get version
|
||||
DWORD verSize = GetFileVersionInfoSizeA(path, nullptr);
|
||||
if (verSize > 0) {
|
||||
auto* verData = (char*)malloc(verSize);
|
||||
if (GetFileVersionInfoA(path, 0, verSize, verData)) {
|
||||
VS_FIXEDFILEINFO* fileInfo = nullptr;
|
||||
UINT len = 0;
|
||||
if (VerQueryValueA(verData, "\\", (void**)&fileInfo, &len)) {
|
||||
printf(" Version: %d.%d.%d.%d\n",
|
||||
HIWORD(fileInfo->dwFileVersionMS),
|
||||
LOWORD(fileInfo->dwFileVersionMS),
|
||||
HIWORD(fileInfo->dwFileVersionLS),
|
||||
LOWORD(fileInfo->dwFileVersionLS));
|
||||
}
|
||||
}
|
||||
free(verData);
|
||||
}
|
||||
} else {
|
||||
printf(" dbgeng.dll not loaded yet\n");
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Done ===\n");
|
||||
return 0;
|
||||
#else
|
||||
printf("Windows only.\n");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
@@ -21,7 +21,7 @@ static QString buildCommandRow(const Provider& prov, uint64_t baseAddress) {
|
||||
QString src = buildSourceLabel(prov);
|
||||
QString addr = QStringLiteral("0x") +
|
||||
QString::number(baseAddress, 16).toUpper();
|
||||
return QStringLiteral(" %1 \u00B7 %2").arg(src, addr);
|
||||
return QStringLiteral(" %1 %2").arg(src, addr);
|
||||
}
|
||||
|
||||
// -- Replicate commandRowSrcSpan for testing
|
||||
@@ -32,17 +32,13 @@ struct TestColumnSpan {
|
||||
};
|
||||
|
||||
static TestColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (idx < 0) return {};
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = 0;
|
||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
||||
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||
if (start >= idx) return {};
|
||||
// Exclude trailing ▾ from the editable span
|
||||
int end = idx;
|
||||
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
if (start >= arrow) return {};
|
||||
return {start, arrow, true};
|
||||
}
|
||||
|
||||
class TestCommandRow : public QObject {
|
||||
@@ -77,13 +73,13 @@ private slots:
|
||||
void row_nullProvider() {
|
||||
NullProvider p;
|
||||
QString row = buildCommandRow(p, 0);
|
||||
QCOMPARE(row, QStringLiteral(" source\u25BE \u00B7 0x0"));
|
||||
QCOMPARE(row, QStringLiteral(" source\u25BE 0x0"));
|
||||
}
|
||||
|
||||
void row_fileProvider() {
|
||||
BufferProvider p(QByteArray(4, '\0'), "test.bin");
|
||||
QString row = buildCommandRow(p, 0x140000000ULL);
|
||||
QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE \u00B7 0x140000000"));
|
||||
QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE 0x140000000"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
@@ -110,7 +106,7 @@ private slots:
|
||||
void span_processProvider_simulated() {
|
||||
// Simulate a process provider without needing Windows APIs
|
||||
// by building the string directly
|
||||
QString row = QStringLiteral(" 'notepad.exe'\u25BE \u00B7 0x7FF600000000");
|
||||
QString row = QStringLiteral(" 'notepad.exe'\u25BE 0x7FF600000000");
|
||||
auto span = commandRowSrcSpan(row);
|
||||
QVERIFY(span.valid);
|
||||
QString extracted = row.mid(span.start, span.end - span.start);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QFile>
|
||||
#include "core.h"
|
||||
|
||||
using namespace rcx;
|
||||
@@ -89,7 +91,7 @@ private slots:
|
||||
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
|
||||
}
|
||||
|
||||
void testPaddingMarker() {
|
||||
void testHexNodeCompose() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -100,19 +102,18 @@ private slots:
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Padding;
|
||||
pad.name = "pad";
|
||||
pad.parentId = rootId;
|
||||
pad.offset = 0;
|
||||
tree.addNode(pad);
|
||||
Node hex;
|
||||
hex.kind = NodeKind::Hex8;
|
||||
hex.name = "pad";
|
||||
hex.parentId = rootId;
|
||||
hex.offset = 0;
|
||||
tree.addNode(hex);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + padding + root footer = 3
|
||||
// CommandRow + hex node + root footer = 3
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
QVERIFY(result.meta[1].markerMask & (1u << M_PAD));
|
||||
QCOMPARE(result.meta[1].depth, 1);
|
||||
|
||||
// Line 2 is root footer
|
||||
@@ -1018,7 +1019,7 @@ private slots:
|
||||
void testPrimitiveArrayElements() {
|
||||
// Expanded primitive array should synthesize element lines dynamically
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -1921,54 +1922,9 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
void testComputeStructAlignment() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Int32 has alignment 4
|
||||
Node f1;
|
||||
f1.kind = NodeKind::Int32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 4);
|
||||
|
||||
// Add Hex64 (alignment 8) — max should become 8
|
||||
Node f2;
|
||||
f2.kind = NodeKind::Hex64;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8;
|
||||
tree.addNode(f2);
|
||||
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 8);
|
||||
}
|
||||
|
||||
void testComputeStructAlignmentEmpty() {
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Empty";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Empty struct → alignment 1
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 1);
|
||||
}
|
||||
|
||||
void testCommandRowRootNameSpan() {
|
||||
// Name span should cover the class name in the merged command row
|
||||
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct\u25BE MyClass {";
|
||||
QString text = "source\u25BE 0x0 struct MyClass {";
|
||||
ColumnSpan nameSpan = commandRowRootNameSpan(text);
|
||||
QVERIFY(nameSpan.valid);
|
||||
|
||||
@@ -1980,7 +1936,7 @@ private slots:
|
||||
void testTextIsNonEmpty() {
|
||||
// Verify composed text is actually generated (not empty)
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -2030,6 +1986,455 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// Union tests
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
void testUnionHeaderShowsKeyword() {
|
||||
// Union (Struct with classKeyword="union") should display "union" in header
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Union container
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.classKeyword = "union";
|
||||
u.name = "u1";
|
||||
u.parentId = rootId;
|
||||
u.offset = 0;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Two members at offset 0
|
||||
Node m1;
|
||||
m1.kind = NodeKind::UInt32;
|
||||
m1.name = "asInt";
|
||||
m1.parentId = uId;
|
||||
m1.offset = 0;
|
||||
tree.addNode(m1);
|
||||
|
||||
Node m2;
|
||||
m2.kind = NodeKind::Float;
|
||||
m2.name = "asFloat";
|
||||
m2.parentId = uId;
|
||||
m2.offset = 0;
|
||||
tree.addNode(m2);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
QStringList lines = result.text.split('\n');
|
||||
|
||||
// Find the union header line
|
||||
int headerLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Header &&
|
||||
result.meta[i].nodeKind == NodeKind::Struct &&
|
||||
result.meta[i].depth == 1) {
|
||||
headerLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(headerLine >= 0);
|
||||
QVERIFY2(lines[headerLine].contains("union"),
|
||||
qPrintable("Union header should contain 'union': " + lines[headerLine]));
|
||||
|
||||
// Both members should be rendered at depth 2
|
||||
int memberCount = 0;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
|
||||
memberCount++;
|
||||
}
|
||||
QCOMPARE(memberCount, 2);
|
||||
|
||||
// Both members share the same offset text (both at 0000)
|
||||
QVector<int> memberLines;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
|
||||
memberLines.append(i);
|
||||
}
|
||||
QCOMPARE(memberLines.size(), 2);
|
||||
QCOMPARE(result.meta[memberLines[0]].offsetText,
|
||||
result.meta[memberLines[1]].offsetText);
|
||||
}
|
||||
|
||||
void testUnionCollapsed() {
|
||||
// Collapsed union should hide children
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.classKeyword = "union";
|
||||
u.name = "u1";
|
||||
u.parentId = rootId;
|
||||
u.offset = 0;
|
||||
u.collapsed = true;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
Node m;
|
||||
m.kind = NodeKind::UInt64;
|
||||
m.name = "val";
|
||||
m.parentId = uId;
|
||||
m.offset = 0;
|
||||
tree.addNode(m);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// No field lines at depth 2
|
||||
int deepFields = 0;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.lineKind == LineKind::Field && lm.depth >= 2)
|
||||
deepFields++;
|
||||
}
|
||||
QCOMPARE(deepFields, 0);
|
||||
}
|
||||
|
||||
void testUnionStructSpan() {
|
||||
// structSpan of a union = max(child offset + size), not sum
|
||||
NodeTree tree;
|
||||
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.classKeyword = "union";
|
||||
u.name = "U";
|
||||
u.parentId = 0;
|
||||
u.offset = 0;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// 2-byte member
|
||||
Node m1;
|
||||
m1.kind = NodeKind::UInt16;
|
||||
m1.name = "small";
|
||||
m1.parentId = uId;
|
||||
m1.offset = 0;
|
||||
tree.addNode(m1);
|
||||
|
||||
// 8-byte member
|
||||
Node m2;
|
||||
m2.kind = NodeKind::UInt64;
|
||||
m2.name = "big";
|
||||
m2.parentId = uId;
|
||||
m2.offset = 0;
|
||||
tree.addNode(m2);
|
||||
|
||||
// structSpan = max(0+2, 0+8) = 8
|
||||
QCOMPARE(tree.structSpan(uId), 8);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// Enum compose tests
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
void testEnumDisplaysMembers() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node e;
|
||||
e.kind = NodeKind::Struct;
|
||||
e.classKeyword = "enum";
|
||||
e.name = "Color";
|
||||
e.structTypeName = "Color";
|
||||
e.parentId = rootId;
|
||||
e.offset = 0;
|
||||
e.collapsed = false;
|
||||
e.enumMembers = {{"Red", 0}, {"Green", 1}, {"Blue", 2}};
|
||||
tree.addNode(e);
|
||||
|
||||
NullProvider prov;
|
||||
auto result = compose(tree, prov);
|
||||
|
||||
// Should have enum members in the text
|
||||
QVERIFY(result.text.contains("Red"));
|
||||
QVERIFY(result.text.contains("Green"));
|
||||
QVERIFY(result.text.contains("Blue"));
|
||||
QVERIFY(result.text.contains("= 0"));
|
||||
QVERIFY(result.text.contains("= 2"));
|
||||
// Header should contain the type name
|
||||
QVERIFY(result.text.contains("Color"));
|
||||
}
|
||||
|
||||
void testEnumCollapsed() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node e;
|
||||
e.kind = NodeKind::Struct;
|
||||
e.classKeyword = "enum";
|
||||
e.name = "Flags";
|
||||
e.structTypeName = "Flags";
|
||||
e.parentId = rootId;
|
||||
e.offset = 0;
|
||||
e.collapsed = true;
|
||||
e.enumMembers = {{"A", 0}, {"B", 1}};
|
||||
tree.addNode(e);
|
||||
|
||||
NullProvider prov;
|
||||
auto result = compose(tree, prov);
|
||||
|
||||
// Collapsed: members should NOT appear
|
||||
QVERIFY(!result.text.contains("= 0"));
|
||||
QVERIFY(!result.text.contains("= 1"));
|
||||
// But header should still show the type name
|
||||
QVERIFY(result.text.contains("Flags"));
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// Compact columns: load EPROCESS.rcx and compare output
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
void testCompactColumnsEprocess() {
|
||||
// Load the EPROCESS example .rcx
|
||||
// Try multiple paths: build dir examples, or source dir
|
||||
QString rcxPath;
|
||||
QStringList candidates = {
|
||||
QCoreApplication::applicationDirPath() + "/examples/EPROCESS.rcx",
|
||||
QCoreApplication::applicationDirPath() + "/../src/examples/EPROCESS.rcx",
|
||||
};
|
||||
for (const auto& c : candidates) {
|
||||
if (QFile::exists(c)) { rcxPath = c; break; }
|
||||
}
|
||||
if (rcxPath.isEmpty())
|
||||
QSKIP("EPROCESS.rcx not found");
|
||||
QFile file(rcxPath);
|
||||
QVERIFY2(file.open(QIODevice::ReadOnly),
|
||||
qPrintable("Cannot open " + rcxPath));
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
|
||||
NodeTree tree = NodeTree::fromJson(jdoc.object());
|
||||
NullProvider prov;
|
||||
|
||||
// Compose WITHOUT compact (default)
|
||||
ComposeResult normal = compose(tree, prov, 0, false);
|
||||
// Compose WITH compact
|
||||
ComposeResult compact = compose(tree, prov, 0, true);
|
||||
|
||||
// Compact typeW should be capped at kCompactTypeW (22)
|
||||
QVERIFY2(compact.layout.typeW <= kCompactTypeW,
|
||||
qPrintable(QString("compact typeW=%1, expected <= %2")
|
||||
.arg(compact.layout.typeW).arg(kCompactTypeW)));
|
||||
|
||||
// Normal typeW should be wider (the _EPROCESS has long type names)
|
||||
QVERIFY2(normal.layout.typeW > compact.layout.typeW,
|
||||
qPrintable(QString("normal typeW=%1 should exceed compact typeW=%2")
|
||||
.arg(normal.layout.typeW).arg(compact.layout.typeW)));
|
||||
|
||||
// Print side-by-side sample for visual inspection
|
||||
QStringList normalLines = normal.text.split('\n');
|
||||
QStringList compactLines = compact.text.split('\n');
|
||||
qDebug() << "\n=== EPROCESS compact columns comparison ===";
|
||||
qDebug() << "Normal typeW:" << normal.layout.typeW
|
||||
<< " Compact typeW:" << compact.layout.typeW;
|
||||
qDebug() << "Normal lines:" << normalLines.size()
|
||||
<< " Compact lines:" << compactLines.size();
|
||||
|
||||
// Dump full output to files for visual diffing
|
||||
{
|
||||
QFile nf(QCoreApplication::applicationDirPath() + "/../eprocess_normal.txt");
|
||||
nf.open(QIODevice::WriteOnly);
|
||||
nf.write(normal.text.toUtf8());
|
||||
}
|
||||
{
|
||||
QFile cf(QCoreApplication::applicationDirPath() + "/../eprocess_compact.txt");
|
||||
cf.open(QIODevice::WriteOnly);
|
||||
cf.write(compact.text.toUtf8());
|
||||
}
|
||||
qDebug() << "Wrote eprocess_normal.txt and eprocess_compact.txt";
|
||||
|
||||
// Show first 50 lines of each for quick inspection
|
||||
qDebug() << "\n--- NORMAL (first 50 lines) ---";
|
||||
for (int i = 0; i < qMin(50, normalLines.size()); ++i)
|
||||
qDebug().noquote() << normalLines[i];
|
||||
|
||||
qDebug() << "\n--- COMPACT (first 50 lines) ---";
|
||||
for (int i = 0; i < qMin(50, compactLines.size()); ++i)
|
||||
qDebug().noquote() << compactLines[i];
|
||||
|
||||
// Overflow types should print in full (no truncation)
|
||||
bool foundFull = false;
|
||||
for (const QString& l : compactLines) {
|
||||
if (l.contains("_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES")) {
|
||||
foundFull = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundFull,
|
||||
"Long type _PS_DYNAMIC_ENFORCED_ADDRESS_RANGES should print in full (no truncation)");
|
||||
}
|
||||
|
||||
void testMmpfnRcxLoadsAndComposes() {
|
||||
// Load the MMPFN.rcx example file and verify it composes without errors
|
||||
// Try several paths to find the .rcx file
|
||||
QString rcxPath;
|
||||
for (const auto& p : {
|
||||
QStringLiteral("../src/examples/MMPFN.rcx"),
|
||||
QStringLiteral("../../src/examples/MMPFN.rcx"),
|
||||
QStringLiteral("src/examples/MMPFN.rcx")}) {
|
||||
if (QFile::exists(p)) { rcxPath = p; break; }
|
||||
}
|
||||
if (rcxPath.isEmpty()) {
|
||||
QSKIP("MMPFN.rcx not found (run from build dir)");
|
||||
}
|
||||
QFile f(rcxPath);
|
||||
QVERIFY2(f.open(QIODevice::ReadOnly), "Cannot open MMPFN.rcx");
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||
QVERIFY(jdoc.isObject());
|
||||
NodeTree tree = NodeTree::fromJson(jdoc.object());
|
||||
|
||||
QVERIFY2(tree.nodes.size() >= 60, "Expected at least 60 nodes");
|
||||
|
||||
// Check key top-level types exist
|
||||
bool hasMmpfn = false, hasListEntry = false, hasMmpte = false;
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.parentId == 0 && n.structTypeName == "_MMPFN") hasMmpfn = true;
|
||||
if (n.parentId == 0 && n.structTypeName == "_LIST_ENTRY") hasListEntry = true;
|
||||
if (n.parentId == 0 && n.structTypeName == "_MMPTE") hasMmpte = true;
|
||||
}
|
||||
QVERIFY2(hasMmpfn, "Missing _MMPFN top-level type");
|
||||
QVERIFY2(hasListEntry, "Missing _LIST_ENTRY top-level type");
|
||||
QVERIFY2(hasMmpte, "Missing _MMPTE top-level type");
|
||||
|
||||
// Compose and verify output
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov, 0, false);
|
||||
QStringList lines = result.text.split('\n');
|
||||
QVERIFY2(lines.size() > 10, "Expected non-trivial compose output");
|
||||
|
||||
// Print first 30 lines for manual inspection
|
||||
qDebug() << "=== MMPFN compose output ===";
|
||||
for (int i = 0; i < qMin(30, lines.size()); ++i)
|
||||
qDebug().noquote() << lines[i];
|
||||
qDebug() << "... total lines:" << lines.size();
|
||||
|
||||
// Verify _MMPFN header appears in output
|
||||
bool foundMmpfn = false;
|
||||
for (const auto& l : lines) {
|
||||
if (l.contains("_MMPFN")) { foundMmpfn = true; break; }
|
||||
}
|
||||
QVERIFY2(foundMmpfn, "Compose output should contain _MMPFN");
|
||||
|
||||
// Verify no M_CYCLE markers on any lines (all self-ref pointers are collapsed)
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
bool hasCycle = (result.meta[i].markerMask & (1u << M_CYCLE)) != 0;
|
||||
QVERIFY2(!hasCycle,
|
||||
qPrintable(QString("Unexpected cycle marker on line %1").arg(i)));
|
||||
}
|
||||
}
|
||||
|
||||
void testBitfieldMembers() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("Test");
|
||||
root.structTypeName = QStringLiteral("Test");
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node bf;
|
||||
bf.kind = NodeKind::Struct;
|
||||
bf.classKeyword = QStringLiteral("bitfield");
|
||||
bf.name = QStringLiteral("flags");
|
||||
bf.elementKind = NodeKind::Hex32;
|
||||
bf.parentId = rootId;
|
||||
bf.offset = 0;
|
||||
bf.collapsed = false;
|
||||
bf.bitfieldMembers = {
|
||||
{QStringLiteral("Valid"), 0, 1},
|
||||
{QStringLiteral("Dirty"), 1, 1},
|
||||
{QStringLiteral("PageNum"), 2, 20}
|
||||
};
|
||||
tree.addNode(bf);
|
||||
|
||||
NullProvider prov;
|
||||
auto result = compose(tree, prov);
|
||||
|
||||
// Should contain bitfield member names
|
||||
QVERIFY(result.text.contains(QStringLiteral("Valid")));
|
||||
QVERIFY(result.text.contains(QStringLiteral("Dirty")));
|
||||
QVERIFY(result.text.contains(QStringLiteral("PageNum")));
|
||||
// Should contain : width = value format
|
||||
QVERIFY(result.text.contains(QStringLiteral(": 1 =")));
|
||||
QVERIFY(result.text.contains(QStringLiteral(": 20 =")));
|
||||
// Member lines should have isMemberLine set
|
||||
bool foundMemberLine = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.isMemberLine) {
|
||||
foundMemberLine = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundMemberLine);
|
||||
}
|
||||
|
||||
void testBitfieldJsonRoundtrip() {
|
||||
Node n;
|
||||
n.id = 42;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = NodeKind::Hex64;
|
||||
n.bitfieldMembers = {
|
||||
{QStringLiteral("ExecuteDisable"), 63, 1},
|
||||
{QStringLiteral("PageFrameNumber"), 12, 36}
|
||||
};
|
||||
|
||||
QJsonObject json = n.toJson();
|
||||
Node restored = Node::fromJson(json);
|
||||
|
||||
QCOMPARE(restored.classKeyword, QStringLiteral("bitfield"));
|
||||
QCOMPARE(restored.bitfieldMembers.size(), 2);
|
||||
QCOMPARE(restored.bitfieldMembers[0].name, QStringLiteral("ExecuteDisable"));
|
||||
QCOMPARE(restored.bitfieldMembers[0].bitOffset, (uint8_t)63);
|
||||
QCOMPARE(restored.bitfieldMembers[0].bitWidth, (uint8_t)1);
|
||||
QCOMPARE(restored.bitfieldMembers[1].name, QStringLiteral("PageFrameNumber"));
|
||||
QCOMPARE(restored.bitfieldMembers[1].bitOffset, (uint8_t)12);
|
||||
QCOMPARE(restored.bitfieldMembers[1].bitWidth, (uint8_t)36);
|
||||
}
|
||||
|
||||
void testBitfieldByteSize() {
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = NodeKind::Hex8;
|
||||
QCOMPARE(n.byteSize(), 1);
|
||||
n.elementKind = NodeKind::Hex16;
|
||||
QCOMPARE(n.byteSize(), 2);
|
||||
n.elementKind = NodeKind::Hex32;
|
||||
QCOMPARE(n.byteSize(), 4);
|
||||
n.elementKind = NodeKind::Hex64;
|
||||
QCOMPARE(n.byteSize(), 8);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCompose)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
using namespace rcx;
|
||||
|
||||
static void buildTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -394,6 +394,65 @@ private slots:
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
|
||||
// ── Change to Ptr* creates class and sets refId ──
|
||||
|
||||
void testChangeToPtrStarCreatesClassAndSetsRef() {
|
||||
// Add a Hex64 node to the root struct
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "ptrField");
|
||||
QApplication::processEvents();
|
||||
|
||||
int ptrIdx = findNode("ptrField");
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
uint64_t ptrNodeId = m_doc->tree.nodes[ptrIdx].id;
|
||||
int before = countNodes();
|
||||
|
||||
// Convert to typed pointer
|
||||
m_ctrl->convertToTypedPointer(ptrNodeId);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Re-find after tree mutation
|
||||
ptrIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
|
||||
}
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
|
||||
// Verify: node kind changed to Pointer64
|
||||
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Pointer64);
|
||||
|
||||
// Verify: node.refId != 0
|
||||
uint64_t refId = m_doc->tree.nodes[ptrIdx].refId;
|
||||
QVERIFY(refId != 0);
|
||||
|
||||
// Verify: a new Struct node exists with the refId as its id
|
||||
int structIdx = m_doc->tree.indexOfId(refId);
|
||||
QVERIFY(structIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[structIdx].kind, NodeKind::Struct);
|
||||
|
||||
// Verify: the new struct has children (Hex64 fields)
|
||||
auto children = m_doc->tree.childrenOf(refId);
|
||||
QVERIFY(children.size() == 16);
|
||||
for (int ci : children)
|
||||
QCOMPARE(m_doc->tree.nodes[ci].kind, NodeKind::Hex64);
|
||||
|
||||
// Verify: total nodes increased by 1 struct + 16 children = 17
|
||||
QCOMPARE(countNodes(), before + 17);
|
||||
|
||||
// Verify: undo restores the original Hex64 kind and refId==0
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
|
||||
ptrIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
|
||||
}
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Hex64);
|
||||
QCOMPARE(m_doc->tree.nodes[ptrIdx].refId, (uint64_t)0);
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestContextMenu)
|
||||
|
||||
@@ -8,10 +8,29 @@
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// Provider with a configurable base address (for testing source-switch logic)
|
||||
class BaseAwareProvider : public Provider {
|
||||
QByteArray m_data;
|
||||
uint64_t m_base;
|
||||
public:
|
||||
BaseAwareProvider(QByteArray data, uint64_t base)
|
||||
: m_data(std::move(data)), m_base(base) {}
|
||||
bool read(uint64_t addr, void* buf, int len) const override {
|
||||
if (addr + len > (uint64_t)m_data.size()) return false;
|
||||
std::memcpy(buf, m_data.constData() + addr, len);
|
||||
return true;
|
||||
}
|
||||
int size() const override { return m_data.size(); }
|
||||
uint64_t base() const override { return m_base; }
|
||||
bool isLive() const override { return true; }
|
||||
QString name() const override { return QStringLiteral("test"); }
|
||||
QString kind() const override { return QStringLiteral("Process"); }
|
||||
};
|
||||
|
||||
// Small tree: one root struct with a few typed fields at known offsets.
|
||||
// Keeps tests fast and deterministic (no giant PEB tree).
|
||||
static void buildSmallTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -34,9 +53,8 @@ static void buildSmallTree(NodeTree& tree) {
|
||||
field(0, NodeKind::UInt32, "field_u32"); // 4 bytes
|
||||
field(4, NodeKind::Float, "field_float"); // 4 bytes
|
||||
field(8, NodeKind::UInt8, "field_u8"); // 1 byte
|
||||
field(9, NodeKind::Padding, "pad0"); // 3 bytes padding
|
||||
// Set padding arrayLen = 3 for 3-byte padding
|
||||
tree.nodes.last().arrayLen = 3;
|
||||
field(9, NodeKind::Hex16, "pad0"); // 2 bytes
|
||||
field(11, NodeKind::Hex8, "pad1"); // 1 byte
|
||||
field(12, NodeKind::Hex32, "field_hex"); // 4 bytes
|
||||
}
|
||||
|
||||
@@ -282,47 +300,6 @@ private slots:
|
||||
QVERIFY(newIdx >= 0);
|
||||
}
|
||||
|
||||
// ── Test: Padding value edit is effectively blocked at controller level ──
|
||||
void testPaddingValueEditIsBlocked() {
|
||||
// Find the padding node
|
||||
int padIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].kind == NodeKind::Padding) { padIdx = i; break; }
|
||||
}
|
||||
QVERIFY(padIdx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(padIdx);
|
||||
|
||||
// Read original data at padding offset
|
||||
int padSize = m_doc->tree.nodes[padIdx].byteSize();
|
||||
QByteArray origData = m_doc->provider->readBytes(addr, padSize);
|
||||
|
||||
// The context menu blocks Padding editing, so the controller's setNodeValue
|
||||
// would only be called if the editing UI somehow allows it. But let's verify
|
||||
// the editor correctly blocks it.
|
||||
// Find padding line in composed output
|
||||
ComposeResult result = m_doc->compose();
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(paddingLine >= 0);
|
||||
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// beginInlineEdit(Value) on Padding line must be rejected
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
|
||||
// Data must be unchanged
|
||||
QByteArray afterData = m_doc->provider->readBytes(addr, padSize);
|
||||
QCOMPARE(afterData, origData);
|
||||
}
|
||||
|
||||
// ── Test: setNodeValue with Hex32 (space-separated hex bytes) ──
|
||||
void testSetNodeValueHex() {
|
||||
int idx = -1;
|
||||
@@ -425,6 +402,44 @@ private slots:
|
||||
QCOMPARE((uint8_t)bytes[0], (uint8_t)0xFF);
|
||||
}
|
||||
|
||||
// ── Test: source switch preserves existing base address ──
|
||||
void testSourceSwitchPreservesBase() {
|
||||
// Set a non-zero baseAddress to simulate a loaded .rcx file
|
||||
m_doc->tree.baseAddress = 0x1000;
|
||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||
|
||||
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
|
||||
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x400000);
|
||||
uint64_t newBase = prov->base();
|
||||
QCOMPARE(newBase, (uint64_t)0x400000);
|
||||
|
||||
m_doc->provider = prov;
|
||||
// Controller logic: keep existing baseAddress when non-zero
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
|
||||
// baseAddress must stay at the original value
|
||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||
// provider base is unchanged (no setBase sync) — provider reports its own initial base
|
||||
QCOMPARE(m_doc->provider->base(), (uint64_t)0x400000);
|
||||
}
|
||||
|
||||
// ── Test: source switch on fresh doc uses provider default ──
|
||||
void testSourceSwitchFreshDocUsesProviderBase() {
|
||||
// Simulate a fresh document (no loaded .rcx → baseAddress == 0)
|
||||
m_doc->tree.baseAddress = 0;
|
||||
|
||||
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x7FFE0000);
|
||||
uint64_t newBase = prov->base();
|
||||
|
||||
m_doc->provider = prov;
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
|
||||
// Fresh doc should adopt the provider's default base
|
||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
|
||||
}
|
||||
|
||||
// ── Test: toggleCollapse + undo ──
|
||||
void testToggleCollapse() {
|
||||
// Root is index 0, a Struct node
|
||||
@@ -448,6 +463,211 @@ private slots:
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
|
||||
}
|
||||
// ── Test: value history popup only appears during inline editing ──
|
||||
void testValueHistoryPopupOnlyDuringEdit() {
|
||||
// Record value history for field_u32 so it has heat
|
||||
auto& tree = m_doc->tree;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
uint64_t nodeId = tree.nodes[idx].id;
|
||||
|
||||
QHash<uint64_t, ValueHistory> history;
|
||||
history[nodeId].record("100");
|
||||
history[nodeId].record("200");
|
||||
history[nodeId].record("300");
|
||||
QVERIFY(history[nodeId].uniqueCount() > 1);
|
||||
|
||||
m_editor->setValueHistoryRef(&history);
|
||||
|
||||
// Refresh and compose so editor has meta with heatLevel
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
ComposeResult result = m_doc->compose();
|
||||
// Manually set heat on the node's line meta
|
||||
for (auto& lm : result.meta) {
|
||||
if (lm.nodeId == nodeId) lm.heatLevel = 2;
|
||||
}
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Popup should not exist or not be visible (no editing active)
|
||||
auto* popup = m_editor->findChild<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
|
||||
// Even if popup widget exists, it should not be visible
|
||||
bool popupVisible = false;
|
||||
for (auto* child : m_editor->findChildren<QFrame*>(QString(), Qt::FindDirectChildrenOnly)) {
|
||||
if (child->isVisible() && child->windowFlags() & Qt::ToolTip)
|
||||
popupVisible = true;
|
||||
}
|
||||
QVERIFY2(!popupVisible, "Popup should not be visible when not editing");
|
||||
|
||||
// Start inline edit on value column of field_u32
|
||||
int fieldLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeId == nodeId && result.meta[i].lineKind == LineKind::Field) {
|
||||
fieldLine = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(fieldLine >= 0);
|
||||
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, fieldLine);
|
||||
QVERIFY(ok);
|
||||
QVERIFY(m_editor->isEditing());
|
||||
|
||||
// Trigger hover cursor update (simulates mouse move during editing)
|
||||
QApplication::processEvents();
|
||||
|
||||
// Cancel edit to clean up
|
||||
m_editor->cancelInlineEdit();
|
||||
QApplication::processEvents();
|
||||
|
||||
m_editor->setValueHistoryRef(nullptr);
|
||||
}
|
||||
|
||||
// ── Test: delete node clears value history for shifted siblings ──
|
||||
void testDeleteClearsHeatForShiftedNodes() {
|
||||
// Replace with a live provider so refresh() actually records values
|
||||
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0x1000);
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
auto& tree = m_doc->tree;
|
||||
|
||||
// Locate field_u32 (the node we'll delete) and the siblings after it.
|
||||
// The small tree has: field_u32(0), field_float(4), field_u8(8),
|
||||
// pad0/Hex16(9), pad1/Hex8(11), field_hex/Hex32(12)
|
||||
// field_float and field_u8 are regular (non-hex) types.
|
||||
int delIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == "field_u32") { delIdx = i; break; }
|
||||
}
|
||||
QVERIFY(delIdx >= 0);
|
||||
uint64_t delId = tree.nodes[delIdx].id;
|
||||
|
||||
// Collect sibling node IDs that come after field_u32 (will be shifted)
|
||||
uint64_t parentId = tree.nodes[delIdx].parentId;
|
||||
int deletedSize = tree.nodes[delIdx].byteSize(); // 4 bytes
|
||||
int deletedEnd = tree.nodes[delIdx].offset + deletedSize;
|
||||
QVector<uint64_t> shiftedIds;
|
||||
QHash<uint64_t, QString> nameMap; // for debug messages
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].parentId == parentId && i != delIdx
|
||||
&& tree.nodes[i].offset >= deletedEnd) {
|
||||
shiftedIds.append(tree.nodes[i].id);
|
||||
nameMap[tree.nodes[i].id] = tree.nodes[i].name;
|
||||
}
|
||||
}
|
||||
QVERIFY2(!shiftedIds.isEmpty(), "Should have siblings after field_u32");
|
||||
|
||||
// Seed value history for shifted siblings (simulate accumulated heat)
|
||||
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
|
||||
for (uint64_t id : shiftedIds) {
|
||||
history[id].record("old_val_1");
|
||||
history[id].record("old_val_2");
|
||||
history[id].record("old_val_3");
|
||||
QVERIFY2(history[id].heatLevel() >= 2,
|
||||
qPrintable(QString("Pre-delete: %1 should have heat>=2")
|
||||
.arg(nameMap[id])));
|
||||
}
|
||||
|
||||
// Also seed the to-be-deleted node
|
||||
history[delId].record("del_1");
|
||||
history[delId].record("del_2");
|
||||
QVERIFY(history.contains(delId));
|
||||
|
||||
// Delete field_u32 — this shifts all subsequent siblings
|
||||
m_ctrl->removeNode(delIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
// The deleted node's history should be gone
|
||||
QVERIFY2(!m_ctrl->valueHistory().contains(delId),
|
||||
"Deleted node's value history should be cleared");
|
||||
|
||||
// All shifted siblings should have heat=0 after the delete.
|
||||
// With a live provider, refresh() inside removeNode re-records one new
|
||||
// value at the new offset → count=1 → heatLevel=0.
|
||||
for (uint64_t id : shiftedIds) {
|
||||
int heat = m_ctrl->valueHistory().contains(id)
|
||||
? m_ctrl->valueHistory()[id].heatLevel() : 0;
|
||||
QVERIFY2(heat == 0,
|
||||
qPrintable(QString("Shifted node '%1' (id=%2) should have heat=0, got %3")
|
||||
.arg(nameMap[id]).arg(id).arg(heat)));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test: value history records and cycles correctly ──
|
||||
void testValueHistoryRingBuffer() {
|
||||
ValueHistory vh;
|
||||
QCOMPARE(vh.count, 0);
|
||||
QCOMPARE(vh.heatLevel(), 0);
|
||||
|
||||
vh.record("10");
|
||||
QCOMPARE(vh.count, 1);
|
||||
QCOMPARE(vh.heatLevel(), 0); // 1 unique = static
|
||||
|
||||
// Duplicate should not increase count
|
||||
vh.record("10");
|
||||
QCOMPARE(vh.count, 1);
|
||||
|
||||
vh.record("20");
|
||||
QCOMPARE(vh.count, 2);
|
||||
QCOMPARE(vh.heatLevel(), 1); // cold
|
||||
|
||||
vh.record("30");
|
||||
QCOMPARE(vh.count, 3);
|
||||
QCOMPARE(vh.heatLevel(), 2); // warm
|
||||
|
||||
vh.record("40");
|
||||
vh.record("50");
|
||||
QCOMPARE(vh.count, 5);
|
||||
QCOMPARE(vh.heatLevel(), 3); // hot
|
||||
|
||||
QCOMPARE(vh.last(), QString("50"));
|
||||
|
||||
// Ring buffer: uniqueCount() caps at kCapacity
|
||||
for (int i = 0; i < 20; i++)
|
||||
vh.record(QString::number(100 + i));
|
||||
QCOMPARE(vh.uniqueCount(), ValueHistory::kCapacity);
|
||||
QVERIFY(vh.count > ValueHistory::kCapacity);
|
||||
|
||||
// forEach iterates oldest→newest within ring
|
||||
QStringList vals;
|
||||
vh.forEach([&](const QString& v) { vals.append(v); });
|
||||
QCOMPARE(vals.size(), ValueHistory::kCapacity);
|
||||
QCOMPARE(vals.last(), vh.last());
|
||||
}
|
||||
// ── Test: inline edit "int32_t[4]" on primitive converts to array ──
|
||||
void testInlineEditPrimitiveArray() {
|
||||
// Find a primitive field to convert
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
|
||||
uint64_t nodeId = m_doc->tree.nodes[idx].id;
|
||||
|
||||
// Emit inlineEditCommitted with array syntax
|
||||
emit m_editor->inlineEditCommitted(idx, 0, EditTarget::Type,
|
||||
QStringLiteral("int32_t[4]"));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Node should now be an Array with elementKind=Int32, arrayLen=4
|
||||
int newIdx = m_doc->tree.indexOfId(nodeId);
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::Array);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].elementKind, NodeKind::Int32);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].arrayLen, 4);
|
||||
|
||||
// Undo should restore to UInt32
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
newIdx = m_doc->tree.indexOfId(nodeId);
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestController)
|
||||
|
||||
@@ -583,6 +583,94 @@ private slots:
|
||||
QCOMPARE(norm.size(), 1);
|
||||
QVERIFY(norm.contains(rootId));
|
||||
}
|
||||
|
||||
// ── ValueHistory tests ──
|
||||
|
||||
void testValueHistory_empty() {
|
||||
rcx::ValueHistory h;
|
||||
QCOMPARE(h.heatLevel(), 0);
|
||||
QCOMPARE(h.uniqueCount(), 0);
|
||||
QCOMPARE(h.last(), QString());
|
||||
}
|
||||
|
||||
void testValueHistory_singleValue() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("42");
|
||||
QCOMPARE(h.heatLevel(), 0); // only 1 unique → static
|
||||
QCOMPARE(h.uniqueCount(), 1);
|
||||
QCOMPARE(h.last(), QString("42"));
|
||||
}
|
||||
|
||||
void testValueHistory_duplicateIgnored() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("42");
|
||||
h.record("42");
|
||||
h.record("42");
|
||||
QCOMPARE(h.count, 1);
|
||||
QCOMPARE(h.heatLevel(), 0);
|
||||
}
|
||||
|
||||
void testValueHistory_heatLevels() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("a");
|
||||
QCOMPARE(h.heatLevel(), 0); // 1 unique
|
||||
|
||||
h.record("b");
|
||||
QCOMPARE(h.heatLevel(), 1); // 2 unique → cold
|
||||
|
||||
h.record("c");
|
||||
QCOMPARE(h.heatLevel(), 2); // 3 unique → warm
|
||||
|
||||
h.record("d");
|
||||
QCOMPARE(h.heatLevel(), 2); // 4 unique → warm
|
||||
|
||||
h.record("e");
|
||||
QCOMPARE(h.heatLevel(), 3); // 5 unique → hot
|
||||
}
|
||||
|
||||
void testValueHistory_ringWrap() {
|
||||
rcx::ValueHistory h;
|
||||
// Fill beyond capacity
|
||||
for (int i = 0; i < 15; i++)
|
||||
h.record(QString::number(i));
|
||||
|
||||
QCOMPARE(h.count, 15);
|
||||
QCOMPARE(h.uniqueCount(), 10); // capped at kCapacity
|
||||
QCOMPARE(h.heatLevel(), 3); // hot
|
||||
QCOMPARE(h.last(), QString("14"));
|
||||
|
||||
// Verify oldest values were pushed out, newest 10 remain
|
||||
QStringList collected;
|
||||
h.forEach([&](const QString& v) { collected.append(v); });
|
||||
QCOMPARE(collected.size(), 10);
|
||||
QCOMPARE(collected.first(), QString("5")); // oldest surviving
|
||||
QCOMPARE(collected.last(), QString("14")); // newest
|
||||
}
|
||||
|
||||
void testValueHistory_forEach() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("x");
|
||||
h.record("y");
|
||||
h.record("z");
|
||||
|
||||
QStringList items;
|
||||
h.forEach([&](const QString& v) { items.append(v); });
|
||||
QCOMPARE(items.size(), 3);
|
||||
QCOMPARE(items[0], QString("x"));
|
||||
QCOMPARE(items[1], QString("y"));
|
||||
QCOMPARE(items[2], QString("z"));
|
||||
}
|
||||
|
||||
void testValueHistory_oscillation() {
|
||||
// Values that oscillate (A → B → A → B) should still count each unique transition
|
||||
rcx::ValueHistory h;
|
||||
h.record("A");
|
||||
h.record("B");
|
||||
h.record("A");
|
||||
h.record("B");
|
||||
QCOMPARE(h.count, 4); // 4 transitions
|
||||
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCore)
|
||||
|
||||
465
tests/test_disasm.cpp
Normal file
465
tests/test_disasm.cpp
Normal file
@@ -0,0 +1,465 @@
|
||||
#include <QtTest/QTest>
|
||||
#include "disasm.h"
|
||||
#include "core.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// Helper: extract mnemonic portion from disassembly output (after "addr ")
|
||||
static QString mnemonic(const QString& line) {
|
||||
int sep = line.indexOf(" ");
|
||||
return sep >= 0 ? line.mid(sep + 2) : line;
|
||||
}
|
||||
|
||||
class TestDisasm : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
// ──────────────────────────────────────────────────
|
||||
// disassemble() unit tests – exact mnemonic match
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
void testDisasm64_pushMov() {
|
||||
QByteArray code("\x55\x48\x89\xe5", 4);
|
||||
QString result = disassemble(code, 0x401000, 64);
|
||||
QStringList lines = result.split('\n');
|
||||
QCOMPARE(lines.size(), 2);
|
||||
QVERIFY(lines[0].startsWith("0000000000401000"));
|
||||
QVERIFY(lines[1].startsWith("0000000000401001"));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
|
||||
}
|
||||
|
||||
void testDisasm64_ret() { QCOMPARE(mnemonic(disassemble(QByteArray("\xc3",1), 0x7FF000, 64)), QStringLiteral("ret")); }
|
||||
void testDisasm64_nop() { QCOMPARE(mnemonic(disassemble(QByteArray("\x90",1), 0, 64)), QStringLiteral("nop")); }
|
||||
void testDisasm64_xorEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x31\xc0",2), 0, 64)), QStringLiteral("xor eax, eax")); }
|
||||
void testDisasm64_subRsp() { QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x83\xec\x20",4), 0, 64)), QStringLiteral("sub rsp, 0x20")); }
|
||||
void testDisasm64_int3() { QCOMPARE(mnemonic(disassemble(QByteArray("\xcc",1), 0, 64)), QStringLiteral("int3")); }
|
||||
void testDisasm64_pushRdi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x57",1), 0, 64)), QStringLiteral("push rdi")); }
|
||||
void testDisasm64_popRsi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x5e",1), 0, 64)), QStringLiteral("pop rsi")); }
|
||||
void testDisasm64_testEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x85\xc0",2), 0, 64)), QStringLiteral("test eax, eax")); }
|
||||
|
||||
void testDisasm64_leaRipRel() {
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8d\x05\x10\x00\x00\x00",7), 0x1000, 64)),
|
||||
QStringLiteral("lea rax, [rip+0x10]"));
|
||||
}
|
||||
void testDisasm64_callRel() {
|
||||
// call target = 0x1000 + 5 + 0x100 = 0x1105
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\xe8\x00\x01\x00\x00",5), 0x1000, 64)),
|
||||
QStringLiteral("call 0x1105"));
|
||||
}
|
||||
void testDisasm64_jmpRel() {
|
||||
// jmp target = 0x1000 + 2 + 0x10 = 0x1012
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\xeb\x10",2), 0x1000, 64)),
|
||||
QStringLiteral("jmp 0x1012"));
|
||||
}
|
||||
void testDisasm64_movMemRead() {
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8b\x43\x10",4), 0, 64)),
|
||||
QStringLiteral("mov rax, qword ptr [rbx+0x10]"));
|
||||
}
|
||||
void testDisasm64_movMemWrite() {
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x89\x4c\x24\x08",5), 0, 64)),
|
||||
QStringLiteral("mov qword ptr [rsp+0x8], rcx"));
|
||||
}
|
||||
|
||||
void testDisasm64_functionPrologue() {
|
||||
QByteArray code("\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
|
||||
QStringList lines = disassemble(code, 0x140001000ULL, 64).split('\n');
|
||||
QCOMPARE(lines.size(), 4);
|
||||
QVERIFY(lines[0].startsWith("0000000140001000"));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
|
||||
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
|
||||
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
|
||||
}
|
||||
|
||||
void testDisasm64_multipleNops() {
|
||||
QStringList lines = disassemble(QByteArray(5,'\x90'), 0x1000, 64).split('\n');
|
||||
QCOMPARE(lines.size(), 5);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
QCOMPARE(mnemonic(lines[i]), QStringLiteral("nop"));
|
||||
QVERIFY(lines[i].startsWith(QStringLiteral("%1").arg(0x1000+i, 16, 16, QLatin1Char('0'))));
|
||||
}
|
||||
}
|
||||
|
||||
void testDisasm32_pushMov() {
|
||||
QByteArray code("\x55\x89\xe5", 3);
|
||||
QStringList lines = disassemble(code, 0x401000, 32).split('\n');
|
||||
QCOMPARE(lines.size(), 2);
|
||||
QVERIFY(lines[0].startsWith("00401000"));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push ebp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov ebp, esp"));
|
||||
}
|
||||
|
||||
void testDisasm_empty() { QVERIFY(disassemble({}, 0, 64).isEmpty()); QVERIFY(disassemble({}, 0, 32).isEmpty()); }
|
||||
void testDisasm_invalidBitness() { QVERIFY(disassemble(QByteArray("\x90",1), 0, 16).isEmpty()); }
|
||||
void testDisasm_maxBytes() { QCOMPARE(disassemble(QByteArray(200,'\x90'), 0, 64, 128).count('\n') + 1, 128); }
|
||||
void testDisasm64_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 64).indexOf(" "), 16); }
|
||||
void testDisasm32_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 32).indexOf(" "), 8); }
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// hexDump() unit tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
void testHexDump_basic() {
|
||||
QByteArray data; for (int i=0;i<32;i++) data.append((char)i);
|
||||
QString r = hexDump(data, 0x1000, 128);
|
||||
QCOMPARE(r.count('\n')+1, 2);
|
||||
QVERIFY(r.startsWith("00001000"));
|
||||
}
|
||||
void testHexDump_ascii() {
|
||||
QVERIFY(hexDump(QByteArray("Hello, World!xx",15), 0, 128).contains("Hello"));
|
||||
}
|
||||
void testHexDump_nonPrintable() {
|
||||
QByteArray d(16,'\0'); d[0]='A'; d[15]='Z';
|
||||
QVERIFY(hexDump(d, 0, 128).contains("A..............Z"));
|
||||
}
|
||||
void testHexDump_empty() { QVERIFY(hexDump({}, 0).isEmpty()); }
|
||||
void testHexDump_maxBytes() { QCOMPARE(hexDump(QByteArray(200,'\xAA'), 0, 64).count('\n')+1, 4); }
|
||||
void testHexDump_wideAddr() { QVERIFY(hexDump(QByteArray(16,'\0'), 0x100000000ULL, 128).startsWith("0000000100000000")); }
|
||||
void testHexDump_hexValues() {
|
||||
QByteArray d; d.append('\xDE'); d.append('\xAD'); d.append('\xBE'); d.append('\xEF');
|
||||
while (d.size()<16) d.append('\0');
|
||||
QVERIFY(hexDump(d, 0, 128).contains("de ad be ef", Qt::CaseInsensitive));
|
||||
}
|
||||
void testHexDump_secondLineAddr() {
|
||||
QStringList lines = hexDump(QByteArray(32,'\x42'), 0x2000, 128).split('\n');
|
||||
QCOMPARE(lines.size(), 2);
|
||||
QVERIFY(lines[1].startsWith("00002010"));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// End-to-end: pointer-expanded VTable with FuncPtr64
|
||||
// Verifies we read from the COMPOSED address, not node.offset
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
void testVTableDisasm_composedAddress() {
|
||||
// Memory layout (absolute addresses, baseAddress = 0):
|
||||
//
|
||||
// [0x0000] Root "Obj" struct
|
||||
// +0x00: Pointer64 __vptr => points to 0x100 (vtable)
|
||||
//
|
||||
// [0x0100] VTable (expanded via pointer deref)
|
||||
// +0x00: func ptr 0 => value 0x200 (func0 code)
|
||||
// +0x08: func ptr 1 => value 0x300 (func1 code)
|
||||
//
|
||||
// [0x0200] func0 code: push rbp; ret
|
||||
// [0x0300] func1 code: xor eax, eax; ret
|
||||
//
|
||||
|
||||
// Build a 4KB buffer
|
||||
QByteArray mem(4096, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) {
|
||||
memcpy(mem.data() + off, &val, 8);
|
||||
};
|
||||
|
||||
// Root object at offset 0: __vptr points to vtable at 0x100
|
||||
w64(0x00, 0x100);
|
||||
|
||||
// VTable at offset 0x100: two function pointers
|
||||
w64(0x100, 0x200); // slot 0 -> func0
|
||||
w64(0x108, 0x300); // slot 1 -> func1
|
||||
|
||||
// func0 at offset 0x200: push rbp; ret
|
||||
mem[0x200] = '\x55';
|
||||
mem[0x201] = '\xc3';
|
||||
|
||||
// func1 at offset 0x300: xor eax, eax; ret
|
||||
mem[0x300] = '\x31';
|
||||
mem[0x301] = '\xc0';
|
||||
mem[0x302] = '\xc3';
|
||||
|
||||
BufferProvider prov(mem);
|
||||
|
||||
// Build node tree
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
// Root struct "Obj"
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Obj";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// VTable struct definition (template)
|
||||
Node vtDef;
|
||||
vtDef.kind = NodeKind::Struct;
|
||||
vtDef.name = "VTable";
|
||||
vtDef.parentId = 0;
|
||||
vtDef.offset = 0x1000; // parked far away so it doesn't overlap
|
||||
int vti = tree.addNode(vtDef);
|
||||
uint64_t vtId = tree.nodes[vti].id;
|
||||
|
||||
// Two FuncPtr64 children inside VTable definition
|
||||
Node fp0;
|
||||
fp0.kind = NodeKind::FuncPtr64;
|
||||
fp0.name = "func0";
|
||||
fp0.parentId = vtId;
|
||||
fp0.offset = 0;
|
||||
tree.addNode(fp0);
|
||||
|
||||
Node fp1;
|
||||
fp1.kind = NodeKind::FuncPtr64;
|
||||
fp1.name = "func1";
|
||||
fp1.parentId = vtId;
|
||||
fp1.offset = 8;
|
||||
tree.addNode(fp1);
|
||||
|
||||
// Pointer64 "__vptr" in root, pointing to VTable via refId
|
||||
Node vptr;
|
||||
vptr.kind = NodeKind::Pointer64;
|
||||
vptr.name = "__vptr";
|
||||
vptr.parentId = rootId;
|
||||
vptr.offset = 0;
|
||||
vptr.refId = vtId;
|
||||
tree.addNode(vptr);
|
||||
|
||||
// Compose the tree
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Find the FuncPtr64 lines in the composed output that are inside the
|
||||
// pointer-expanded VTable (near vtable address), not the standalone definition.
|
||||
struct FuncInfo { int line; uint64_t offsetAddr; NodeKind kind; QString name; };
|
||||
QVector<FuncInfo> funcPtrs;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
const LineMeta& lm = result.meta[i];
|
||||
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
|
||||
// Only include the pointer-expanded ones (near vtable at 0x100)
|
||||
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
|
||||
int nodeIdx = lm.nodeIdx;
|
||||
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
|
||||
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QCOMPARE(funcPtrs.size(), 2);
|
||||
|
||||
// Verify composed addresses point to the vtable, NOT to the root struct
|
||||
// func0 should be at 0x100 (vtable + 0)
|
||||
QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
|
||||
// func1 should be at 0x108 (vtable + 8)
|
||||
QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
|
||||
|
||||
// Now simulate what the hover code should do:
|
||||
// Read the function pointer VALUE from the correct provider address
|
||||
for (const auto& fp : funcPtrs) {
|
||||
// Provider reads at absolute address directly
|
||||
uint64_t provAddr = fp.offsetAddr;
|
||||
|
||||
// Read the pointer value (the function address)
|
||||
uint64_t ptrVal = prov.readU64(provAddr);
|
||||
|
||||
// Verify we got the right pointer values
|
||||
if (fp.name == "func0") {
|
||||
QCOMPARE(ptrVal, (uint64_t)0x200);
|
||||
} else {
|
||||
QCOMPARE(ptrVal, (uint64_t)0x300);
|
||||
}
|
||||
|
||||
// Read code bytes at the pointer target (absolute address)
|
||||
uint64_t codeProvAddr = ptrVal;
|
||||
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
|
||||
|
||||
// Disassemble and verify
|
||||
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
|
||||
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for " + fp.name));
|
||||
|
||||
QStringList lines = asm_.split('\n');
|
||||
if (fp.name == "func0") {
|
||||
// Should decode: push rbp; ret
|
||||
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func0, got %1: %2").arg(lines.size()).arg(asm_)));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
// Verify address in output matches the real function address
|
||||
QVERIFY2(lines[0].contains("200"),
|
||||
qPrintable("func0 addr wrong: " + lines[0]));
|
||||
} else {
|
||||
// Should decode: xor eax, eax; ret
|
||||
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_)));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
QVERIFY2(lines[0].contains("300"),
|
||||
qPrintable("func1 addr wrong: " + lines[0]));
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Verify that reading from node.offset (the WRONG way) gives
|
||||
// different/wrong results. node.offset for func0=0, func1=8, which are
|
||||
// inside the ROOT struct, not the vtable.
|
||||
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
|
||||
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
|
||||
// wrongVal0 = 0x100 (the vptr itself, NOT a function address)
|
||||
QCOMPARE(wrongVal0, (uint64_t)0x100);
|
||||
// This is the vtable address, not a function — disassembling it would be wrong
|
||||
QVERIFY2(wrongVal0 != (uint64_t)0x200,
|
||||
"node.offset reads the vptr, not the function pointer");
|
||||
QVERIFY2(wrongVal1 != (uint64_t)0x300,
|
||||
"node.offset=8 reads past vptr, not the second function pointer");
|
||||
}
|
||||
|
||||
void testVTableDisasm_wrongAddressGivesWrongCode() {
|
||||
// Demonstrate that using node.offset instead of composed address
|
||||
// gives completely wrong disassembly results
|
||||
QByteArray mem(1024, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
|
||||
|
||||
// Root at 0: vptr -> 0x80
|
||||
w64(0x00, (uint64_t)0x80);
|
||||
// VTable at 0x80: one func ptr -> 0x100
|
||||
w64(0x80, (uint64_t)0x100);
|
||||
// Code at 0x100: sub rsp, 0x28; nop; ret
|
||||
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
|
||||
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
|
||||
|
||||
BufferProvider prov(mem);
|
||||
|
||||
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
|
||||
uint64_t wrongPtrVal = prov.readU64(0);
|
||||
QCOMPARE(wrongPtrVal, (uint64_t)0x80); // This is the vtable addr, not a function!
|
||||
|
||||
// RIGHT: read from composed address (vtable + 0)
|
||||
uint64_t rightPtrVal = prov.readU64(0x80);
|
||||
QCOMPARE(rightPtrVal, (uint64_t)0x100); // This IS the function address
|
||||
|
||||
// Disassemble the RIGHT target
|
||||
QByteArray rightCode = prov.readBytes(0x100, 128);
|
||||
QString rightAsm = disassemble(rightCode, 0x100, 64, 128);
|
||||
QStringList rightLines = rightAsm.split('\n');
|
||||
QVERIFY(rightLines.size() >= 3);
|
||||
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
|
||||
QCOMPARE(mnemonic(rightLines[1]), QStringLiteral("nop"));
|
||||
QCOMPARE(mnemonic(rightLines[2]), QStringLiteral("ret"));
|
||||
|
||||
// Disassemble the WRONG target (vtable data, not code!)
|
||||
QByteArray wrongCode = prov.readBytes(0x80, 128);
|
||||
QString wrongAsm = disassemble(wrongCode, 0x80, 64, 128);
|
||||
// The wrong bytes are the vtable entries (pointer values),
|
||||
// which decode as garbage instructions, not sub/nop/ret
|
||||
QVERIFY2(!wrongAsm.contains("sub rsp"),
|
||||
qPrintable("Wrong address should NOT produce sub rsp: " + wrongAsm));
|
||||
}
|
||||
|
||||
void testHoverFlow_fullSimulation() {
|
||||
// Full simulation of the hover flow as implemented in editor.cpp:
|
||||
//
|
||||
// 1. Compose the tree to get LineMeta with correct offsetAddr
|
||||
// 2. For each FuncPtr64 line, read pointer value from provider
|
||||
// using lm.offsetAddr (absolute address)
|
||||
// 3. Read code bytes from the REAL provider using ptrVal directly
|
||||
// (the real provider can read any process address; snapshot cannot)
|
||||
// 4. Disassemble the code bytes
|
||||
//
|
||||
// The key distinction: step 2 reads from composed tree addresses (in
|
||||
// the snapshot), step 3 reads from arbitrary code addresses (needs
|
||||
// the real provider, not snapshot).
|
||||
|
||||
QByteArray mem(8192, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) {
|
||||
memcpy(mem.data() + off, &val, 8);
|
||||
};
|
||||
|
||||
// Layout:
|
||||
// [0x000] Root struct: __vptr -> vtable at 0x100
|
||||
// [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
|
||||
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
|
||||
// [0x1800] func1 code: xor eax, eax; ret
|
||||
w64(0x000, (uint64_t)0x100); // __vptr
|
||||
w64(0x100, (uint64_t)0x1000); // vtable[0]
|
||||
w64(0x108, (uint64_t)0x1800); // vtable[1]
|
||||
// func0 code
|
||||
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
|
||||
// func1 code
|
||||
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
|
||||
|
||||
// This provider represents the real process memory.
|
||||
BufferProvider realProv(mem);
|
||||
|
||||
// Build a snapshot that only contains tree-data pages (like the
|
||||
// async refresh does). The snapshot does NOT contain function code pages.
|
||||
// This simulates the real scenario where SnapshotProvider only has
|
||||
// pages for the root struct and pointer-expanded structs.
|
||||
QByteArray snapData(0x200, '\0'); // only pages for root + vtable
|
||||
memcpy(snapData.data(), mem.constData(), 0x200);
|
||||
BufferProvider snapProv(snapData);
|
||||
|
||||
// Build node tree
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
|
||||
root.parentId = 0; root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node vtDef; vtDef.kind = NodeKind::Struct; vtDef.name = "VTable";
|
||||
vtDef.parentId = 0; vtDef.offset = 0x2000;
|
||||
int vti = tree.addNode(vtDef);
|
||||
uint64_t vtId = tree.nodes[vti].id;
|
||||
|
||||
Node fp0; fp0.kind = NodeKind::FuncPtr64; fp0.name = "func0";
|
||||
fp0.parentId = vtId; fp0.offset = 0;
|
||||
tree.addNode(fp0);
|
||||
Node fp1; fp1.kind = NodeKind::FuncPtr64; fp1.name = "func1";
|
||||
fp1.parentId = vtId; fp1.offset = 8;
|
||||
tree.addNode(fp1);
|
||||
|
||||
Node vptr; vptr.kind = NodeKind::Pointer64; vptr.name = "__vptr";
|
||||
vptr.parentId = rootId; vptr.offset = 0; vptr.refId = vtId;
|
||||
tree.addNode(vptr);
|
||||
|
||||
// Compose with the snapshot (like production: compose uses snapshot)
|
||||
ComposeResult result = compose(tree, snapProv);
|
||||
|
||||
// Find expanded FuncPtr64 lines
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
const LineMeta& lm = result.meta[i];
|
||||
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
|
||||
continue;
|
||||
if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
|
||||
continue; // skip standalone VTable definition entries
|
||||
|
||||
// --- Hover step 1: read pointer value from snapshot ---
|
||||
uint64_t provAddr = lm.offsetAddr;
|
||||
// The snapshot has this data (vtable pages are in it)
|
||||
QVERIFY2(snapProv.isReadable(provAddr, 8),
|
||||
qPrintable(QString("Snapshot should have vtable page at %1")
|
||||
.arg(provAddr, 0, 16)));
|
||||
uint64_t ptrVal = snapProv.readU64(provAddr);
|
||||
QVERIFY2(ptrVal != 0, "Function pointer should not be zero");
|
||||
|
||||
// --- Hover step 2: read code from REAL provider ---
|
||||
// The snapshot does NOT have the code pages:
|
||||
uint64_t codeAddr = ptrVal;
|
||||
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
|
||||
"Snapshot should NOT have function code pages");
|
||||
// But the real provider does:
|
||||
QByteArray codeBytes(128, Qt::Uninitialized);
|
||||
bool readOk = realProv.read(codeAddr, codeBytes.data(), 128);
|
||||
QVERIFY2(readOk, "Real provider should be able to read code bytes");
|
||||
|
||||
// --- Hover step 3: disassemble ---
|
||||
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
|
||||
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for line " + QString::number(i)));
|
||||
|
||||
QStringList lines = asm_.split('\n');
|
||||
const Node& node = tree.nodes[lm.nodeIdx];
|
||||
if (node.name == "func0") {
|
||||
QVERIFY(lines.size() >= 4);
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
|
||||
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
|
||||
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
|
||||
} else if (node.name == "func1") {
|
||||
QVERIFY(lines.size() >= 2);
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestDisasm)
|
||||
#include "test_disasm.moc"
|
||||
File diff suppressed because it is too large
Load Diff
360
tests/test_export_xml.cpp
Normal file
360
tests/test_export_xml.cpp
Normal file
@@ -0,0 +1,360 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include <QTemporaryFile>
|
||||
#include "core.h"
|
||||
#include "imports/export_reclass_xml.h"
|
||||
#include "imports/import_reclass_xml.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestExportXml : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void exportEmptyTree();
|
||||
void exportSingleStruct();
|
||||
void exportPointerRef();
|
||||
void exportEmbeddedStruct();
|
||||
void exportArray();
|
||||
void exportTextNodes();
|
||||
void exportVectors();
|
||||
void exportHexCollapse();
|
||||
void exportMultiClass();
|
||||
void roundTripImportExport();
|
||||
};
|
||||
|
||||
static int countRoots(const NodeTree& tree) {
|
||||
int n = 0;
|
||||
for (const auto& node : tree.nodes)
|
||||
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
|
||||
QVector<int> result;
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
if (tree.nodes[i].parentId == parentId) result.append(i);
|
||||
return result;
|
||||
}
|
||||
|
||||
static QString exportToString(const NodeTree& tree) {
|
||||
QTemporaryFile tmp;
|
||||
tmp.setAutoRemove(true);
|
||||
if (!tmp.open()) return {};
|
||||
QString path = tmp.fileName();
|
||||
tmp.close();
|
||||
|
||||
QString err;
|
||||
if (!exportReclassXml(tree, path, &err)) return {};
|
||||
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||
return QString::fromUtf8(f.readAll());
|
||||
}
|
||||
|
||||
static NodeTree roundTrip(const NodeTree& tree) {
|
||||
QTemporaryFile tmp;
|
||||
tmp.setAutoRemove(true);
|
||||
if (!tmp.open()) return {};
|
||||
QString path = tmp.fileName();
|
||||
tmp.close();
|
||||
|
||||
QString err;
|
||||
if (!exportReclassXml(tree, path, &err)) return {};
|
||||
return importReclassXml(path, &err);
|
||||
}
|
||||
|
||||
// ── Tests ──
|
||||
|
||||
void TestExportXml::exportEmptyTree() {
|
||||
NodeTree tree;
|
||||
QString err;
|
||||
QVERIFY(!exportReclassXml(tree, "dummy.xml", &err));
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
void TestExportXml::exportSingleStruct() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Player");
|
||||
s.structTypeName = QStringLiteral("Player"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node f1; f1.kind = NodeKind::Int32; f1.name = QStringLiteral("health");
|
||||
f1.parentId = sid; f1.offset = 0; tree.addNode(f1);
|
||||
|
||||
Node f2; f2.kind = NodeKind::Float; f2.name = QStringLiteral("speed");
|
||||
f2.parentId = sid; f2.offset = 4; tree.addNode(f2);
|
||||
|
||||
Node f3; f3.kind = NodeKind::UInt64; f3.name = QStringLiteral("id");
|
||||
f3.parentId = sid; f3.offset = 8; tree.addNode(f3);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(!xml.isEmpty());
|
||||
QVERIFY(xml.contains(QStringLiteral("Player")));
|
||||
QVERIFY(xml.contains(QStringLiteral("health")));
|
||||
QVERIFY(xml.contains(QStringLiteral("speed")));
|
||||
QVERIFY(xml.contains(QStringLiteral("ReClassEx")));
|
||||
|
||||
// Round-trip
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
QCOMPARE(rt.nodes[0].name, QStringLiteral("Player"));
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Int32);
|
||||
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Float);
|
||||
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::UInt64);
|
||||
}
|
||||
|
||||
void TestExportXml::exportPointerRef() {
|
||||
NodeTree tree;
|
||||
Node s1; s1.kind = NodeKind::Struct; s1.name = QStringLiteral("Target");
|
||||
s1.structTypeName = QStringLiteral("Target"); s1.parentId = 0;
|
||||
int s1i = tree.addNode(s1);
|
||||
uint64_t s1id = tree.nodes[s1i].id;
|
||||
|
||||
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||
f.parentId = s1id; f.offset = 0; tree.addNode(f);
|
||||
|
||||
Node s2; s2.kind = NodeKind::Struct; s2.name = QStringLiteral("HasPtr");
|
||||
s2.structTypeName = QStringLiteral("HasPtr"); s2.parentId = 0;
|
||||
int s2i = tree.addNode(s2);
|
||||
uint64_t s2id = tree.nodes[s2i].id;
|
||||
|
||||
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("pTarget");
|
||||
ptr.parentId = s2id; ptr.offset = 0; ptr.refId = s1id;
|
||||
tree.addNode(ptr);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(xml.contains(QStringLiteral("Pointer=\"Target\"")));
|
||||
|
||||
// Round-trip: pointer should resolve
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 2);
|
||||
bool foundPtr = false;
|
||||
for (const auto& n : rt.nodes) {
|
||||
if (n.kind == NodeKind::Pointer64 && n.name == QStringLiteral("pTarget")) {
|
||||
QVERIFY(n.refId != 0);
|
||||
foundPtr = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundPtr);
|
||||
}
|
||||
|
||||
void TestExportXml::exportEmbeddedStruct() {
|
||||
NodeTree tree;
|
||||
Node inner; inner.kind = NodeKind::Struct; inner.name = QStringLiteral("Inner");
|
||||
inner.structTypeName = QStringLiteral("Inner"); inner.parentId = 0;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t iid = tree.nodes[ii].id;
|
||||
|
||||
Node iv; iv.kind = NodeKind::Int32; iv.name = QStringLiteral("x");
|
||||
iv.parentId = iid; iv.offset = 0; tree.addNode(iv);
|
||||
|
||||
Node outer; outer.kind = NodeKind::Struct; outer.name = QStringLiteral("Outer");
|
||||
outer.structTypeName = QStringLiteral("Outer"); outer.parentId = 0;
|
||||
int oi = tree.addNode(outer);
|
||||
uint64_t oid = tree.nodes[oi].id;
|
||||
|
||||
Node embed; embed.kind = NodeKind::Struct; embed.name = QStringLiteral("embedded");
|
||||
embed.structTypeName = QStringLiteral("Inner"); embed.parentId = oid;
|
||||
embed.offset = 0; embed.refId = iid;
|
||||
tree.addNode(embed);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(xml.contains(QStringLiteral("Instance=\"Inner\"")));
|
||||
}
|
||||
|
||||
void TestExportXml::exportArray() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Container");
|
||||
s.structTypeName = QStringLiteral("Container"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node arr; arr.kind = NodeKind::Array; arr.name = QStringLiteral("items");
|
||||
arr.parentId = sid; arr.offset = 0; arr.arrayLen = 10;
|
||||
arr.elementKind = NodeKind::Int32;
|
||||
tree.addNode(arr);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(xml.contains(QStringLiteral("Total=\"10\"")));
|
||||
QVERIFY(xml.contains(QStringLiteral("<Array")));
|
||||
}
|
||||
|
||||
void TestExportXml::exportTextNodes() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("TextStruct");
|
||||
s.structTypeName = QStringLiteral("TextStruct"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("name");
|
||||
u8.parentId = sid; u8.offset = 0; u8.strLen = 32; tree.addNode(u8);
|
||||
|
||||
Node u16; u16.kind = NodeKind::UTF16; u16.name = QStringLiteral("wname");
|
||||
u16.parentId = sid; u16.offset = 32; u16.strLen = 16; tree.addNode(u16);
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::UTF8);
|
||||
QCOMPARE(rt.nodes[kids[0]].strLen, 32);
|
||||
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::UTF16);
|
||||
QCOMPARE(rt.nodes[kids[1]].strLen, 16);
|
||||
}
|
||||
|
||||
void TestExportXml::exportVectors() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Vectors");
|
||||
s.structTypeName = QStringLiteral("Vectors"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node v2; v2.kind = NodeKind::Vec2; v2.name = QStringLiteral("pos2");
|
||||
v2.parentId = sid; v2.offset = 0; tree.addNode(v2);
|
||||
|
||||
Node v3; v3.kind = NodeKind::Vec3; v3.name = QStringLiteral("pos3");
|
||||
v3.parentId = sid; v3.offset = 8; tree.addNode(v3);
|
||||
|
||||
Node v4; v4.kind = NodeKind::Vec4; v4.name = QStringLiteral("rot");
|
||||
v4.parentId = sid; v4.offset = 20; tree.addNode(v4);
|
||||
|
||||
Node m; m.kind = NodeKind::Mat4x4; m.name = QStringLiteral("matrix");
|
||||
m.parentId = sid; m.offset = 36; tree.addNode(m);
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 4);
|
||||
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Vec2);
|
||||
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Vec3);
|
||||
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::Vec4);
|
||||
QCOMPARE(rt.nodes[kids[3]].kind, NodeKind::Mat4x4);
|
||||
}
|
||||
|
||||
void TestExportXml::exportHexCollapse() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("HexTest");
|
||||
s.structTypeName = QStringLiteral("HexTest"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
// 4 consecutive Hex8 nodes should collapse to one Custom node
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Node h; h.kind = NodeKind::Hex8; h.parentId = sid; h.offset = i;
|
||||
tree.addNode(h);
|
||||
}
|
||||
// Followed by a real field
|
||||
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||
f.parentId = sid; f.offset = 4; tree.addNode(f);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
// Should have Type="21" (Custom) for the collapsed hex
|
||||
QVERIFY(xml.contains(QStringLiteral("Type=\"21\"")));
|
||||
// Size should be 4
|
||||
QVERIFY(xml.contains(QStringLiteral("Size=\"4\"")));
|
||||
|
||||
// Round-trip: custom expands back to hex nodes
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
// Import expands Custom(4 bytes) to best-fit hex: Hex32 (1 node) + Int32 = 2
|
||||
QVERIFY(kids.size() >= 2);
|
||||
// Last child should be Int32
|
||||
QCOMPARE(rt.nodes[kids.last()].kind, NodeKind::Int32);
|
||||
}
|
||||
|
||||
void TestExportXml::exportMultiClass() {
|
||||
NodeTree tree;
|
||||
for (int c = 0; c < 5; c++) {
|
||||
Node s; s.kind = NodeKind::Struct;
|
||||
s.name = QStringLiteral("Class%1").arg(c);
|
||||
s.structTypeName = s.name; s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node f; f.kind = NodeKind::Int32;
|
||||
f.name = QStringLiteral("field%1").arg(c);
|
||||
f.parentId = sid; f.offset = 0; tree.addNode(f);
|
||||
}
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 5);
|
||||
|
||||
// All class names preserved
|
||||
QSet<QString> names;
|
||||
for (const auto& n : rt.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) names.insert(n.name);
|
||||
for (int c = 0; c < 5; c++)
|
||||
QVERIFY(names.contains(QStringLiteral("Class%1").arg(c)));
|
||||
}
|
||||
|
||||
void TestExportXml::roundTripImportExport() {
|
||||
// Build a comprehensive tree and verify it survives export->import
|
||||
NodeTree tree;
|
||||
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("FullTest");
|
||||
s.structTypeName = QStringLiteral("FullTest"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
int offset = 0;
|
||||
auto addField = [&](NodeKind kind, const QString& name) {
|
||||
Node n; n.kind = kind; n.name = name; n.parentId = sid; n.offset = offset;
|
||||
tree.addNode(n);
|
||||
offset += sizeForKind(kind);
|
||||
};
|
||||
|
||||
addField(NodeKind::Int8, QStringLiteral("a"));
|
||||
addField(NodeKind::Int16, QStringLiteral("b"));
|
||||
addField(NodeKind::Int32, QStringLiteral("c"));
|
||||
addField(NodeKind::Int64, QStringLiteral("d"));
|
||||
addField(NodeKind::UInt8, QStringLiteral("e"));
|
||||
addField(NodeKind::UInt16, QStringLiteral("f"));
|
||||
addField(NodeKind::UInt32, QStringLiteral("g"));
|
||||
addField(NodeKind::UInt64, QStringLiteral("h"));
|
||||
addField(NodeKind::Float, QStringLiteral("i"));
|
||||
addField(NodeKind::Double, QStringLiteral("j"));
|
||||
addField(NodeKind::Vec2, QStringLiteral("k"));
|
||||
addField(NodeKind::Vec3, QStringLiteral("l"));
|
||||
addField(NodeKind::Vec4, QStringLiteral("m"));
|
||||
|
||||
// Self-pointer
|
||||
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("self");
|
||||
ptr.parentId = sid; ptr.offset = offset; ptr.refId = sid;
|
||||
tree.addNode(ptr);
|
||||
offset += 8;
|
||||
|
||||
// UTF8
|
||||
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("str");
|
||||
u8.parentId = sid; u8.offset = offset; u8.strLen = 64;
|
||||
tree.addNode(u8);
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
QCOMPARE(rt.nodes[0].name, QStringLiteral("FullTest"));
|
||||
|
||||
auto origKids = childrenOf(tree, sid);
|
||||
auto rtKids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(rtKids.size(), origKids.size());
|
||||
|
||||
// Verify each field kind matches
|
||||
for (int i = 0; i < origKids.size(); i++) {
|
||||
QCOMPARE(rt.nodes[rtKids[i]].kind, tree.nodes[origKids[i]].kind);
|
||||
QCOMPARE(rt.nodes[rtKids[i]].name, tree.nodes[origKids[i]].name);
|
||||
}
|
||||
|
||||
// Verify self-pointer resolved
|
||||
bool foundSelf = false;
|
||||
for (const auto& n : rt.nodes) {
|
||||
if (n.name == QStringLiteral("self") && n.kind == NodeKind::Pointer64) {
|
||||
QVERIFY(n.refId != 0);
|
||||
QCOMPARE(n.refId, rt.nodes[0].id);
|
||||
foundSelf = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundSelf);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestExportXml)
|
||||
#include "test_export_xml.moc"
|
||||
@@ -29,12 +29,12 @@ private slots:
|
||||
}
|
||||
|
||||
void testFmtPointer64_null() {
|
||||
QCOMPARE(fmt::fmtPointer64(0), QString("-> NULL"));
|
||||
QCOMPARE(fmt::fmtPointer64(0), QString("0x0"));
|
||||
}
|
||||
|
||||
void testFmtPointer64_nonNull() {
|
||||
QString s = fmt::fmtPointer64(0x400000);
|
||||
QVERIFY(s.startsWith("-> 0x"));
|
||||
QVERIFY(s.startsWith("0x"));
|
||||
QVERIFY(s.contains("400000"));
|
||||
}
|
||||
|
||||
|
||||
@@ -418,30 +418,6 @@ private slots:
|
||||
QVERIFY(result.contains("wchar_t wname[32];"));
|
||||
}
|
||||
|
||||
// ── Padding node ──
|
||||
|
||||
void testPaddingNode() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "PadTest";
|
||||
root.structTypeName = "PadTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node pad;
|
||||
pad.kind = rcx::NodeKind::Padding;
|
||||
pad.name = "reserved";
|
||||
pad.parentId = rootId;
|
||||
pad.offset = 0;
|
||||
pad.arrayLen = 16;
|
||||
tree.addNode(pad);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
QVERIFY(result.contains("uint8_t reserved[16];"));
|
||||
}
|
||||
|
||||
// ── Full SDK export (multiple root structs) ──
|
||||
|
||||
void testFullSdkExport() {
|
||||
|
||||
237
tests/test_import_pdb.cpp
Normal file
237
tests/test_import_pdb.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "imports/import_pdb.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestImportPdb : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void missingFileReturnsError();
|
||||
void importKProcess();
|
||||
void verifyDispatcherHeader();
|
||||
void verifyListEntry();
|
||||
void importFilteredStruct();
|
||||
void enumerateTypes();
|
||||
void importSelected();
|
||||
};
|
||||
|
||||
static const QString kPdbPath = QStringLiteral(
|
||||
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
|
||||
|
||||
// Find a root struct by structTypeName
|
||||
static int findRootStruct(const NodeTree& tree, const QString& name) {
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].parentId == 0 &&
|
||||
tree.nodes[i].kind == NodeKind::Struct &&
|
||||
tree.nodes[i].structTypeName == name)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Find a child of parentId by name
|
||||
static int findChildNode(const NodeTree& tree, uint64_t parentId, const QString& name) {
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].parentId == parentId && tree.nodes[i].name == name)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TestImportPdb::missingFileReturnsError() {
|
||||
QString err;
|
||||
NodeTree tree = importPdb(QStringLiteral("C:/nonexistent.pdb"), {}, &err);
|
||||
QVERIFY(tree.nodes.isEmpty());
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
void TestImportPdb::importKProcess() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// Find _KPROCESS root struct
|
||||
int kpIdx = findRootStruct(tree, QStringLiteral("_KPROCESS"));
|
||||
QVERIFY2(kpIdx >= 0, "Expected _KPROCESS root struct");
|
||||
uint64_t kpId = tree.nodes[kpIdx].id;
|
||||
|
||||
// Verify Header field at offset 0 → embedded _DISPATCHER_HEADER
|
||||
int headerIdx = findChildNode(tree, kpId, QStringLiteral("Header"));
|
||||
QVERIFY2(headerIdx >= 0, "Expected 'Header' child of _KPROCESS");
|
||||
QCOMPARE(tree.nodes[headerIdx].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[headerIdx].structTypeName, QStringLiteral("_DISPATCHER_HEADER"));
|
||||
QCOMPARE(tree.nodes[headerIdx].offset, 0);
|
||||
|
||||
// Verify ProfileListHead at offset 0x18 → embedded _LIST_ENTRY
|
||||
int profileIdx = findChildNode(tree, kpId, QStringLiteral("ProfileListHead"));
|
||||
QVERIFY2(profileIdx >= 0, "Expected 'ProfileListHead' child of _KPROCESS");
|
||||
QCOMPARE(tree.nodes[profileIdx].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[profileIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
|
||||
QCOMPARE(tree.nodes[profileIdx].offset, 0x18);
|
||||
}
|
||||
|
||||
void TestImportPdb::verifyDispatcherHeader() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// _DISPATCHER_HEADER should be imported as a transitive dependency
|
||||
int dhIdx = findRootStruct(tree, QStringLiteral("_DISPATCHER_HEADER"));
|
||||
QVERIFY2(dhIdx >= 0, "_DISPATCHER_HEADER should be imported as a dependency");
|
||||
|
||||
uint64_t dhId = tree.nodes[dhIdx].id;
|
||||
auto kids = tree.childrenOf(dhId);
|
||||
QVERIFY2(!kids.isEmpty(), "_DISPATCHER_HEADER should have children (fields)");
|
||||
|
||||
// Look for WaitListHead — a _LIST_ENTRY at offset 0x10 in most builds
|
||||
int waitIdx = findChildNode(tree, dhId, QStringLiteral("WaitListHead"));
|
||||
QVERIFY2(waitIdx >= 0, "Expected 'WaitListHead' in _DISPATCHER_HEADER");
|
||||
QCOMPARE(tree.nodes[waitIdx].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[waitIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
|
||||
}
|
||||
|
||||
void TestImportPdb::verifyListEntry() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// _LIST_ENTRY should be imported (used by ProfileListHead and others)
|
||||
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
|
||||
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
|
||||
|
||||
uint64_t leId = tree.nodes[leIdx].id;
|
||||
|
||||
// Flink at offset 0 — pointer to _LIST_ENTRY
|
||||
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
|
||||
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[flinkIdx].offset, 0);
|
||||
|
||||
// Blink at offset 8 — pointer to _LIST_ENTRY
|
||||
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
|
||||
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[blinkIdx].offset, 8);
|
||||
|
||||
// Both should point back to _LIST_ENTRY (self-referencing)
|
||||
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
|
||||
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
|
||||
}
|
||||
|
||||
void TestImportPdb::importFilteredStruct() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_LIST_ENTRY"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
|
||||
QVERIFY(leIdx >= 0);
|
||||
|
||||
// _LIST_ENTRY only references itself, so exactly 1 root struct
|
||||
int rootCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
||||
QCOMPARE(rootCount, 1);
|
||||
}
|
||||
|
||||
void TestImportPdb::enumerateTypes() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
|
||||
// Should have hundreds of types in ntkrnlmp
|
||||
QVERIFY2(types.size() > 100,
|
||||
qPrintable(QStringLiteral("Expected >100 types, got %1").arg(types.size())));
|
||||
|
||||
// Verify _KPROCESS is present
|
||||
bool foundKProcess = false;
|
||||
bool foundListEntry = false;
|
||||
for (const auto& t : types) {
|
||||
if (t.name == QStringLiteral("_KPROCESS")) {
|
||||
foundKProcess = true;
|
||||
QVERIFY2(t.childCount > 0, "_KPROCESS should have children");
|
||||
QVERIFY2(t.size > 0, "_KPROCESS should have non-zero size");
|
||||
}
|
||||
if (t.name == QStringLiteral("_LIST_ENTRY")) {
|
||||
foundListEntry = true;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundKProcess, "_KPROCESS not found in enumerated types");
|
||||
QVERIFY2(foundListEntry, "_LIST_ENTRY not found in enumerated types");
|
||||
}
|
||||
|
||||
void TestImportPdb::importSelected() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
// First enumerate to find _LIST_ENTRY's type index
|
||||
QString err;
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
|
||||
uint32_t listEntryIdx = 0;
|
||||
bool found = false;
|
||||
for (const auto& t : types) {
|
||||
if (t.name == QStringLiteral("_LIST_ENTRY")) {
|
||||
listEntryIdx = t.typeIndex;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(found, "_LIST_ENTRY not found in enumeration");
|
||||
|
||||
// Import just _LIST_ENTRY
|
||||
QVector<uint32_t> indices = { listEntryIdx };
|
||||
int progressCalls = 0;
|
||||
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
|
||||
[&](int cur, int total) -> bool {
|
||||
progressCalls++;
|
||||
Q_UNUSED(total);
|
||||
Q_ASSERT(cur <= total);
|
||||
return true; // don't cancel
|
||||
});
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
QVERIFY(progressCalls > 0);
|
||||
|
||||
// Verify _LIST_ENTRY root struct
|
||||
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
|
||||
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
|
||||
|
||||
// Flink and Blink
|
||||
uint64_t leId = tree.nodes[leIdx].id;
|
||||
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
|
||||
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
|
||||
|
||||
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
|
||||
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
|
||||
|
||||
// Self-referencing pointers
|
||||
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
|
||||
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
|
||||
|
||||
// Only 1 root struct
|
||||
int rootCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
||||
QCOMPARE(rootCount, 1);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestImportPdb)
|
||||
#include "test_import_pdb.moc"
|
||||
1048
tests/test_import_source.cpp
Normal file
1048
tests/test_import_source.cpp
Normal file
File diff suppressed because it is too large
Load Diff
70
tests/test_import_xml.cpp
Normal file
70
tests/test_import_xml.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "imports/import_reclass_xml.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestImportXml : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void importSmallXml();
|
||||
};
|
||||
|
||||
void TestImportXml::importSmallXml() {
|
||||
// Create a minimal XML in a temp file and test parsing
|
||||
QTemporaryFile tmp;
|
||||
tmp.setAutoRemove(true);
|
||||
QVERIFY(tmp.open());
|
||||
tmp.write(R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ReClass>
|
||||
<!--ReClassEx-->
|
||||
<Class Name="TestClass" Type="28" Comment="" Offset="0" strOffset="0" Code="">
|
||||
<Node Name="vtable" Type="9" Size="8" bHidden="false" Comment=""/>
|
||||
<Node Name="health" Type="13" Size="4" bHidden="false" Comment=""/>
|
||||
<Node Name="name" Type="18" Size="32" bHidden="false" Comment=""/>
|
||||
<Node Name="position" Type="23" Size="12" bHidden="false" Comment=""/>
|
||||
<Node Name="pNext" Type="8" Size="8" bHidden="false" Comment="" Pointer="TestClass"/>
|
||||
</Class>
|
||||
</ReClass>
|
||||
)");
|
||||
tmp.flush();
|
||||
|
||||
QString error;
|
||||
NodeTree tree = importReclassXml(tmp.fileName(), &error);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||
|
||||
// Should have 1 root struct + 5 children = 6 nodes
|
||||
QCOMPARE(tree.nodes.size(), 6);
|
||||
|
||||
// Root struct
|
||||
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[0].name, QStringLiteral("TestClass"));
|
||||
|
||||
// vtable = Int64
|
||||
QCOMPARE(tree.nodes[1].kind, NodeKind::Int64);
|
||||
QCOMPARE(tree.nodes[1].name, QStringLiteral("vtable"));
|
||||
QCOMPARE(tree.nodes[1].offset, 0);
|
||||
|
||||
// health = Float
|
||||
QCOMPARE(tree.nodes[2].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[2].name, QStringLiteral("health"));
|
||||
QCOMPARE(tree.nodes[2].offset, 8);
|
||||
|
||||
// name = UTF8 with strLen=32
|
||||
QCOMPARE(tree.nodes[3].kind, NodeKind::UTF8);
|
||||
QCOMPARE(tree.nodes[3].strLen, 32);
|
||||
QCOMPARE(tree.nodes[3].offset, 12);
|
||||
|
||||
// position = Vec3
|
||||
QCOMPARE(tree.nodes[4].kind, NodeKind::Vec3);
|
||||
QCOMPARE(tree.nodes[4].offset, 44);
|
||||
|
||||
// pNext = Pointer64 with resolved refId
|
||||
QCOMPARE(tree.nodes[5].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[5].name, QStringLiteral("pNext"));
|
||||
QVERIFY(tree.nodes[5].refId != 0);
|
||||
QCOMPARE(tree.nodes[5].refId, tree.nodes[0].id); // points to TestClass
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestImportXml)
|
||||
#include "test_import_xml.moc"
|
||||
@@ -304,39 +304,6 @@ private slots:
|
||||
QVERIFY(result.contains("float speed;"));
|
||||
}
|
||||
|
||||
void testGenerator_typeAliases_padding() {
|
||||
// Padding gap and tail padding should use aliased uint8_t
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "PadTest";
|
||||
root.structTypeName = "PadTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
Node f2;
|
||||
f2.kind = NodeKind::UInt32;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8; // gap of 4 bytes at offset 4
|
||||
tree.addNode(f2);
|
||||
|
||||
QHash<NodeKind, QString> aliases;
|
||||
aliases[NodeKind::Padding] = "BYTE";
|
||||
|
||||
QString result = renderCpp(tree, rootId, &aliases);
|
||||
// Padding gap should use the alias
|
||||
QVERIFY(result.contains("BYTE _pad"));
|
||||
}
|
||||
|
||||
void testGenerator_typeAliases_array() {
|
||||
// Array element type should use alias
|
||||
NodeTree tree;
|
||||
@@ -547,134 +514,92 @@ private slots:
|
||||
void testWorkspace_simpleTree() {
|
||||
auto tree = makeSimpleTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "TestProject.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// 1 top-level item (the project)
|
||||
// Single "Project" root
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->text(), QString("TestProject.rcx"));
|
||||
QCOMPARE(project->text(), QString("Project"));
|
||||
|
||||
// Project has 1 child: the Player struct
|
||||
// 1 type directly under Project: Player (no member fields)
|
||||
QCOMPARE(project->rowCount(), 1);
|
||||
QStandardItem* player = project->child(0);
|
||||
QVERIFY(player->text().contains("Player"));
|
||||
QVERIFY(player->text().contains("struct"));
|
||||
|
||||
// Player struct has 2 children: health, speed
|
||||
QCOMPARE(player->rowCount(), 2);
|
||||
QVERIFY(player->child(0)->text().contains("health"));
|
||||
QVERIFY(player->child(1)->text().contains("speed"));
|
||||
QVERIFY(project->child(0)->text().contains("Player"));
|
||||
QVERIFY(project->child(0)->text().contains("struct"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_twoRootTree() {
|
||||
auto tree = makeTwoRootTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "TwoRoot.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
|
||||
// 2 root struct children: Alpha and Bravo
|
||||
// 2 types sorted alphabetically: Alpha, Bravo (no field children)
|
||||
QCOMPARE(project->rowCount(), 2);
|
||||
QVERIFY(project->child(0)->text().contains("Alpha"));
|
||||
QVERIFY(project->child(1)->text().contains("Bravo"));
|
||||
|
||||
// Each has 1 field child
|
||||
QCOMPARE(project->child(0)->rowCount(), 1);
|
||||
QVERIFY(project->child(0)->child(0)->text().contains("flagsA"));
|
||||
QCOMPARE(project->child(1)->rowCount(), 1);
|
||||
QVERIFY(project->child(1)->child(0)->text().contains("flagsB"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_rootCount() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->rowCount(), 3); // Pet, Cat, Ball
|
||||
QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted)
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_petChildren() {
|
||||
void testWorkspace_richTree_sorted() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* pet = model.item(0)->child(0);
|
||||
QVERIFY(pet->text().contains("Pet"));
|
||||
// Pet has 2 non-hex children: name (UTF8), owner (Pointer64)
|
||||
QCOMPARE(pet->rowCount(), 2);
|
||||
QVERIFY(pet->child(0)->text().contains("name"));
|
||||
QVERIFY(pet->child(1)->text().contains("owner"));
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_catNesting() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
|
||||
QStandardItem* cat = model.item(0)->child(1);
|
||||
QVERIFY(cat->text().contains("Cat"));
|
||||
|
||||
// Find the nested "Pet" struct child (base)
|
||||
QStandardItem* base = nullptr;
|
||||
for (int i = 0; i < cat->rowCount(); i++) {
|
||||
if (cat->child(i)->text().contains("Pet") &&
|
||||
cat->child(i)->text().contains("struct")) {
|
||||
base = cat->child(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(base != nullptr, "Cat should have a nested Pet struct child");
|
||||
|
||||
// base has structId set
|
||||
QVERIFY(base->data(Qt::UserRole + 1).isValid());
|
||||
|
||||
// base should have its own children (name + owner)
|
||||
QCOMPARE(base->rowCount(), 2);
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_ballChildren() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
|
||||
QStandardItem* ball = model.item(0)->child(2);
|
||||
QVERIFY(ball->text().contains("Ball"));
|
||||
|
||||
// Ball has 3 non-hex children: speed, position, color
|
||||
QCOMPARE(ball->rowCount(), 3);
|
||||
QVERIFY(ball->child(0)->text().contains("speed"));
|
||||
QVERIFY(ball->child(1)->text().contains("position"));
|
||||
QVERIFY(ball->child(2)->text().contains("color"));
|
||||
QStandardItem* project = model.item(0);
|
||||
// Sorted alphabetically: Ball, Cat, Pet
|
||||
QVERIFY(project->child(0)->text().contains("Ball"));
|
||||
QVERIFY(project->child(1)->text().contains("Cat"));
|
||||
QVERIFY(project->child(2)->text().contains("Pet"));
|
||||
// No member fields under type nodes
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
QCOMPARE(project->child(2)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_emptyTree() {
|
||||
NodeTree tree;
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Empty.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// Still has the "Project" root, just no children
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(model.item(0)->text(), QString("Project"));
|
||||
QCOMPARE(model.item(0)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_structIdRole() {
|
||||
auto tree = makeSimpleTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Test.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
// Project item should NOT have structId
|
||||
QVERIFY(!project->data(Qt::UserRole + 1).isValid());
|
||||
// Project root has kGroupSentinel
|
||||
QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
|
||||
|
||||
// Player struct should have structId
|
||||
// Player type item should have structId
|
||||
QStandardItem* player = project->child(0);
|
||||
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
||||
|
||||
// health field should NOT have structId
|
||||
QStandardItem* health = player->child(0);
|
||||
QVERIFY(!health->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user