mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
78 Commits
snapshot-1
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86499e58ee | ||
|
|
b2ae8d5a5d | ||
|
|
6768f04e9a | ||
|
|
c6e5f6508f | ||
|
|
e6529052b3 | ||
|
|
d43e989992 | ||
|
|
879e9f4047 | ||
|
|
e0d5a799b4 | ||
|
|
efae193520 | ||
|
|
ba1c2f8e5a | ||
|
|
5a0a4d1802 | ||
|
|
030eb34510 | ||
|
|
2939b25895 | ||
|
|
d38cb02fa2 | ||
|
|
9f285b37b2 | ||
|
|
cae599a0c6 | ||
|
|
d0734ba8be | ||
|
|
696ff044ac | ||
|
|
da312ccac6 | ||
|
|
552b45b16c | ||
|
|
e89fd4a6c1 | ||
|
|
7524004b32 | ||
|
|
ed8a44917b | ||
|
|
ecfac3decf | ||
|
|
851d744263 | ||
|
|
41e2f9f662 | ||
|
|
95faf027a9 | ||
|
|
6a51c904de | ||
|
|
0d73575ea7 | ||
|
|
aa04cfcb5c | ||
|
|
1465e7fbed | ||
|
|
52f751e751 | ||
|
|
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 |
178
.github/workflows/build.yml
vendored
178
.github/workflows/build.yml
vendored
@@ -2,7 +2,8 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
@@ -18,55 +19,38 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6
|
||||
- name: Install Qt6 and MinGW
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
arch: 'win64_msvc2022_64'
|
||||
version: "6.8.1"
|
||||
arch: "win64_mingw"
|
||||
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
|
||||
cache: true
|
||||
aqtversion: '==3.1.21'
|
||||
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: x64
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_controller|test_windbg_provider|test_com_security"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: Reclass-win64-qt6
|
||||
path: |
|
||||
build/Reclass.exe
|
||||
build/ReclassMcpBridge.exe
|
||||
build/Plugins/*.dll
|
||||
build/*.dll
|
||||
build/platforms/
|
||||
build/styles/
|
||||
build/imageformats/
|
||||
build/iconengines/
|
||||
build/themes/
|
||||
build/examples/
|
||||
build/screenshot.png
|
||||
|
||||
- name: Get date tag
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: date
|
||||
shell: bash
|
||||
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Package release zip
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
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/
|
||||
@@ -75,6 +59,7 @@ jobs:
|
||||
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
|
||||
@@ -82,22 +67,13 @@ jobs:
|
||||
cp build/screenshot.png release/ 2>/dev/null || true
|
||||
cd release && 7z a ../Reclass-win64-qt6.zip *
|
||||
|
||||
- name: Upload release asset
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: softprops/action-gh-release@v2
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
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: Reclass-win64-qt6.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Reclass-win64-qt6
|
||||
path: Reclass-win64-qt6.zip
|
||||
|
||||
linux:
|
||||
needs: windows
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -108,9 +84,8 @@ jobs:
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
version: "6.8.1"
|
||||
cache: true
|
||||
aqtversion: '==3.1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -118,15 +93,13 @@ jobs:
|
||||
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
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: xvfb-run ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_controller"
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Create AppImage
|
||||
run: |
|
||||
@@ -164,19 +137,83 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
macos:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-15
|
||||
qt_arch: clang_arm64
|
||||
artifact_name: Reclass-macos-arm64-qt6
|
||||
zip_name: Reclass-macos-arm64-qt6.zip
|
||||
- os: macos-15-intel
|
||||
qt_arch: clang_64
|
||||
artifact_name: Reclass-macos-x86_64-qt6
|
||||
zip_name: Reclass-macos-x86_64-qt6.zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install cmake ninja qt
|
||||
|
||||
- name: Configure Qt paths
|
||||
run: |
|
||||
QT_PREFIX="$(brew --prefix qt)"
|
||||
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
|
||||
echo "PATH=$QT_PREFIX/bin:$PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Package app zip
|
||||
run: |
|
||||
MACDEPLOYQT_BIN="$QT_PREFIX/bin/macdeployqt"
|
||||
if [ ! -x "$MACDEPLOYQT_BIN" ]; then
|
||||
MACDEPLOYQT_BIN=$(which macdeployqt 2>/dev/null || find "$RUNNER_WORKSPACE" -name macdeployqt -path "*/bin/*" | head -1)
|
||||
fi
|
||||
echo "Found macdeployqt at: $MACDEPLOYQT_BIN"
|
||||
"$MACDEPLOYQT_BIN" build/Reclass.app -always-overwrite
|
||||
codesign --force --deep --sign - build/Reclass.app
|
||||
ditto -c -k --sequesterRsrc --keepParent build/Reclass.app "${{ matrix.zip_name }}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.zip_name }}
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [windows, linux, macos]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Get date tag
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: date
|
||||
shell: bash
|
||||
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload release asset
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||
@@ -185,7 +222,10 @@ jobs:
|
||||
Automated snapshot from main branch.
|
||||
Commit: ${{ github.sha }}
|
||||
prerelease: false
|
||||
files: Reclass-linux64-qt6.AppImage
|
||||
files: |
|
||||
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
||||
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
|
||||
artifacts/Reclass-macos-arm64-qt6/Reclass-macos-arm64-qt6.zip
|
||||
artifacts/Reclass-macos-x86_64-qt6/Reclass-macos-x86_64-qt6.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,3 +11,7 @@ build/
|
||||
*.suo
|
||||
.vs/
|
||||
CMakeUserPresets.json
|
||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||
.cache
|
||||
*.DS_Store
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,3 +1,9 @@
|
||||
[submodule "third_party/qscintilla"]
|
||||
path = third_party/qscintilla
|
||||
url = https://github.com/brCreate/QScintilla.git
|
||||
[submodule "third_party/raw_pdb"]
|
||||
path = third_party/raw_pdb
|
||||
url = https://github.com/MolecularMatters/raw_pdb.git
|
||||
[submodule "third_party/fadec"]
|
||||
path = third_party/fadec
|
||||
url = https://github.com/aengelke/fadec.git
|
||||
|
||||
345
CMakeLists.txt
345
CMakeLists.txt
@@ -31,6 +31,40 @@ 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)
|
||||
# PDB_CRT.h forward-declares printf/memcmp/etc with __cdecl which conflicts
|
||||
# with non-MSVC compilers (GCC, Clang, MinGW). Force-include a prefix header
|
||||
# that pulls in the real CRT headers and strips __cdecl.
|
||||
if(NOT MSVC)
|
||||
target_compile_options(raw_pdb PUBLIC
|
||||
-include "${CMAKE_CURRENT_SOURCE_DIR}/cmake/raw_pdb_prefix.h")
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(raw_pdb PRIVATE rpcrt4)
|
||||
endif()
|
||||
|
||||
# Fadec — generate decode tables (.inc files) from instrs.txt at configure time
|
||||
find_package(Python3 3.9 REQUIRED)
|
||||
set(FADEC_DIR "${CMAKE_SOURCE_DIR}/third_party/fadec")
|
||||
if(NOT EXISTS "${FADEC_DIR}/fadec-decode-public.inc")
|
||||
message(STATUS "Generating fadec decode tables...")
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} "${FADEC_DIR}/parseinstrs.py" decode
|
||||
"${FADEC_DIR}/instrs.txt"
|
||||
"${FADEC_DIR}/fadec-decode-public.inc"
|
||||
"${FADEC_DIR}/fadec-decode-private.inc"
|
||||
--32 --64
|
||||
RESULT_VARIABLE _fadec_result
|
||||
)
|
||||
if(NOT _fadec_result EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to generate fadec decode tables")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(Reclass
|
||||
src/main.cpp
|
||||
src/editor.h
|
||||
@@ -60,19 +94,31 @@ add_executable(Reclass
|
||||
src/themes/thememanager.cpp
|
||||
src/themes/themeeditor.h
|
||||
src/themes/themeeditor.cpp
|
||||
src/import_reclass_xml.h
|
||||
src/import_reclass_xml.cpp
|
||||
src/import_source.h
|
||||
src/import_source.cpp
|
||||
src/export_reclass_xml.h
|
||||
src/export_reclass_xml.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/scanner.h
|
||||
src/scanner.cpp
|
||||
src/scannerpanel.h
|
||||
src/scannerpanel.cpp
|
||||
src/mainwindow.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/macos_titlebar.h
|
||||
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/addressparser.h
|
||||
src/addressparser.cpp
|
||||
src/disasm.h
|
||||
src/disasm.cpp
|
||||
third_party/fadec/decode.c
|
||||
@@ -80,6 +126,16 @@ add_executable(Reclass
|
||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(Reclass PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_ICON_FILE "class.icns"
|
||||
)
|
||||
target_sources(Reclass PRIVATE src/icons/class.icns)
|
||||
set_source_files_properties(src/icons/class.icns
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||
endif()
|
||||
|
||||
target_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||
|
||||
target_link_libraries(Reclass PRIVATE
|
||||
@@ -92,7 +148,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)
|
||||
@@ -106,6 +162,12 @@ foreach(_tf ${_theme_files})
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_theme_files})
|
||||
set_source_files_properties(${_theme_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
|
||||
endif()
|
||||
|
||||
# Copy example .rcx files to build directory
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
@@ -114,6 +176,12 @@ foreach(_ef ${_example_files})
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_example_files})
|
||||
set_source_files_properties(${_example_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
|
||||
endif()
|
||||
|
||||
include(deploy)
|
||||
|
||||
|
||||
@@ -154,17 +222,17 @@ if(BUILD_TESTING)
|
||||
|
||||
# ── 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)
|
||||
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)
|
||||
@@ -180,156 +248,252 @@ if(BUILD_TESTING)
|
||||
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/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/import_reclass_xml.cpp src/format.cpp src/compose.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/import_source.cpp src/format.cpp src/compose.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/export_reclass_xml.cpp src/import_reclass_xml.cpp src/format.cpp src/compose.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/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)
|
||||
|
||||
add_executable(test_static_fields tests/test_static_fields.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_static_fields PRIVATE src)
|
||||
target_link_libraries(test_static_fields PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_static_fields COMMAND test_static_fields)
|
||||
|
||||
add_executable(test_scanner tests/test_scanner.cpp src/scanner.cpp)
|
||||
target_include_directories(test_scanner PRIVATE src)
|
||||
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner COMMAND test_scanner)
|
||||
|
||||
add_executable(test_32bit_support tests/test_32bit_support.cpp
|
||||
src/generator.cpp src/imports/import_source.cpp src/imports/import_reclass_xml.cpp
|
||||
src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_32bit_support PRIVATE src
|
||||
${CMAKE_SOURCE_DIR}/plugins/RemoteProcessMemory)
|
||||
target_link_libraries(test_32bit_support PRIVATE ${QT}::Core ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_32bit_support COMMAND test_32bit_support)
|
||||
set_tests_properties(test_32bit_support PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
|
||||
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
|
||||
add_executable(test_controller tests/test_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 ${DISASM_SRCS})
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
add_executable(test_validation tests/test_validation.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_validation PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
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)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp
|
||||
add_executable(test_source_management tests/test_source_management.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/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
|
||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_new_features PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
target_include_directories(test_new_features PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_new_features COMMAND test_new_features)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_new_features COMMAND test_new_features)
|
||||
|
||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
|
||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
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_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_visibility COMMAND test_type_visibility)
|
||||
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
|
||||
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
|
||||
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)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
|
||||
src/scanner.cpp src/scannerpanel.cpp src/addressparser.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_scanner_ui PRIVATE src)
|
||||
target_link_libraries(test_scanner_ui PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
|
||||
src/scanner.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
endif()
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
endif()
|
||||
|
||||
# Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect
|
||||
# Requires a running WinDbg debug server on port 5055
|
||||
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)
|
||||
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)
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
add_custom_target(deploy_tests ALL
|
||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
add_custom_target(deploy_tests ALL
|
||||
COMMAND $<TARGET_FILE:${QT}::windeployqt>
|
||||
--no-compiler-runtime --no-translations
|
||||
--no-opengl-sw --no-system-d3d-compiler
|
||||
@@ -337,11 +501,14 @@ if(BUILD_TESTING)
|
||||
DEPENDS test_controller
|
||||
COMMENT "Deploying Qt runtime DLLs for tests..."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
if(NOT APPLE)
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
|
||||
157
README.md
157
README.md
@@ -1,43 +1,142 @@
|
||||
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">
|
||||
|
||||
## State
|
||||
<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>
|
||||
|
||||
- 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": []
|
||||
}
|
||||
**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**
|
||||
|
||||
[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 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": []
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
## 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/)
|
||||
```/dev/null/commands.sh#L1-4
|
||||
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
|
||||
### macOS Build
|
||||
|
||||
```/dev/null/commands.sh#L1-2
|
||||
./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package
|
||||
```
|
||||
|
||||
If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`.
|
||||
|
||||
Note: macOS Gatekeeper may block unsigned apps. If the app won’t open, go to **System Settings → Privacy & Security** and click **Open Anyway**.
|
||||
|
||||
### Manual Build (MinGW)
|
||||
|
||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||
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
|
||||
|
||||
### Visual Studio 2022+
|
||||
|
||||
The `msvc/` folder contains a ready-made solution (`Reclass.slnx`) with projects for the main application, all plugins, and third-party libraries. Requires the [Qt Visual Studio Tools](https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2022) extension with a Qt 6 MSVC kit configured.
|
||||
|
||||
### 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>
|
||||
|
||||
29
cmake/raw_pdb_prefix.h
Normal file
29
cmake/raw_pdb_prefix.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// Force-included before every raw_pdb translation unit (and consumers).
|
||||
// PDB_CRT.h forward-declares printf/memcmp/etc with extern "C" __cdecl,
|
||||
// which conflicts with MinGW's CRT headers (C++ linkage, no __cdecl).
|
||||
//
|
||||
// Fix: include the real CRT headers, then include PDB_CRT.h with function
|
||||
// names macro-renamed to harmless dummies. This triggers #pragma once so
|
||||
// no raw_pdb source file ever processes PDB_CRT.h's conflicting declarations.
|
||||
//
|
||||
// Guarded with __cplusplus because PUBLIC propagation applies this to C
|
||||
// sources (fadec) where PDB_CRT.h is irrelevant and <cstdio> doesn't exist.
|
||||
#ifdef __cplusplus
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#undef __cdecl
|
||||
#define __cdecl
|
||||
|
||||
#define printf _pdb_crt_unused_printf
|
||||
#define memcmp _pdb_crt_unused_memcmp
|
||||
#define memcpy _pdb_crt_unused_memcpy
|
||||
#define strlen _pdb_crt_unused_strlen
|
||||
#define strcmp _pdb_crt_unused_strcmp
|
||||
#include "Foundation/PDB_CRT.h"
|
||||
#undef printf
|
||||
#undef memcmp
|
||||
#undef memcpy
|
||||
#undef strlen
|
||||
#undef strcmp
|
||||
#endif
|
||||
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 |
117
msvc/ProcessMemoryPlugin.vcxproj
Normal file
117
msvc/ProcessMemoryPlugin.vcxproj
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{8792F51B-4951-4BAD-B130-2F0EFDEFF64B}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>ProcessMemoryPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
124
msvc/RcNetCompatPlugin.vcxproj
Normal file
124
msvc/RcNetCompatPlugin.vcxproj
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{6B775E9C-9CB6-45FD-86A0-BE948A778969}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>RcNetCompatPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.h" />
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.h" />
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ReClassNET_Plugin.hpp" />
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ClrHost.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.cpp" />
|
||||
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.cpp" />
|
||||
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\ClrHost.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
17
msvc/Reclass.slnx
Normal file
17
msvc/Reclass.slnx
Normal file
@@ -0,0 +1,17 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="x64" />
|
||||
</Configurations>
|
||||
<Folder Name="/plugins/">
|
||||
<Project Path="ProcessMemoryPlugin.vcxproj" Id="8792f51b-4951-4bad-b130-2f0efdeff64b" />
|
||||
<Project Path="WinDbgMemoryPlugin.vcxproj" Id="e25d358e-20f0-448b-bb2f-55e9d1f8e7ca" />
|
||||
<Project Path="RemoteProcessMemoryPlugin.vcxproj" Id="39e2ddf6-cb76-4063-b957-66ecf1252010" />
|
||||
<Project Path="RcNetCompatPlugin.vcxproj" Id="6b775e9c-9cb6-45fd-86a0-be948a778969" />
|
||||
</Folder>
|
||||
<Folder Name="/third_party/">
|
||||
<Project Path="../third_party/raw_pdb/build/RawPDB.vcxproj" Id="fbe3dbfa-20a7-4f99-9326-ed82c8b7b910" />
|
||||
<Project Path="fadec.vcxproj" Id="6a30a4f0-1a8d-4c6e-82d4-0a0d9693aa40" />
|
||||
<Project Path="qscintilla.vcxproj" Id="f7124b57-7682-4702-b725-4d844dc41ada" />
|
||||
</Folder>
|
||||
<Project Path="Reclass.vcxproj" Id="c369f1fe-37c2-4c66-ac6d-ecb2b2b4ad5e" />
|
||||
</Solution>
|
||||
191
msvc/Reclass.vcxproj
Normal file
191
msvc/Reclass.vcxproj
Normal file
@@ -0,0 +1,191 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="18.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{C369F1FE-37C2-4C66-AC6D-ECB2B2B4AD5E}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0</WindowsTargetPlatformVersion>
|
||||
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Link>
|
||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ClCompile>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ClCompile>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtRcc Include="..\src\resources.qrc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\controller.h" />
|
||||
<QtMoc Include="..\src\editor.h" />
|
||||
<QtMoc Include="..\src\mainwindow.h" />
|
||||
<QtMoc Include="..\src\optionsdialog.h" />
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
<QtMoc Include="..\src\scanner.h" />
|
||||
<QtMoc Include="..\src\scannerpanel.h" />
|
||||
<QtMoc Include="..\src\titlebar.h" />
|
||||
<QtMoc Include="..\src\typeselectorpopup.h" />
|
||||
<QtMoc Include="..\src\imports\import_pdb_dialog.h" />
|
||||
<QtMoc Include="..\src\mcp\mcp_bridge.h" />
|
||||
<QtMoc Include="..\src\themes\themeeditor.h" />
|
||||
<QtMoc Include="..\src\themes\thememanager.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\addressparser.h" />
|
||||
<ClInclude Include="..\src\core.h" />
|
||||
<ClInclude Include="..\src\disasm.h" />
|
||||
<ClInclude Include="..\src\generator.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\pluginmanager.h" />
|
||||
<ClInclude Include="..\src\providerregistry.h" />
|
||||
<ClInclude Include="..\src\workspace_model.h" />
|
||||
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
|
||||
<ClInclude Include="..\src\imports\import_pdb.h" />
|
||||
<ClInclude Include="..\src\imports\import_reclass_xml.h" />
|
||||
<ClInclude Include="..\src\imports\import_source.h" />
|
||||
<ClInclude Include="..\src\providers\buffer_provider.h" />
|
||||
<ClInclude Include="..\src\providers\null_provider.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
<ClInclude Include="..\src\providers\snapshot_provider.h" />
|
||||
<ClInclude Include="..\src\themes\theme.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\src\addressparser.cpp" />
|
||||
<ClCompile Include="..\src\compose.cpp" />
|
||||
<ClCompile Include="..\src\controller.cpp" />
|
||||
<ClCompile Include="..\src\disasm.cpp" />
|
||||
<ClCompile Include="..\src\editor.cpp" />
|
||||
<ClCompile Include="..\src\format.cpp" />
|
||||
<ClCompile Include="..\src\generator.cpp" />
|
||||
<ClCompile Include="..\src\main.cpp" />
|
||||
<ClCompile Include="..\src\optionsdialog.cpp" />
|
||||
<ClCompile Include="..\src\pluginmanager.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
<ClCompile Include="..\src\providerregistry.cpp" />
|
||||
<ClCompile Include="..\src\scanner.cpp" />
|
||||
<ClCompile Include="..\src\scannerpanel.cpp" />
|
||||
<ClCompile Include="..\src\titlebar.cpp" />
|
||||
<ClCompile Include="..\src\typeselectorpopup.cpp" />
|
||||
<ClCompile Include="..\src\imports\export_reclass_xml.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_pdb.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_reclass_xml.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_source.cpp" />
|
||||
<ClCompile Include="..\src\mcp\mcp_bridge.cpp" />
|
||||
<ClCompile Include="..\src\themes\theme.cpp" />
|
||||
<ClCompile Include="..\src\themes\themeeditor.cpp" />
|
||||
<ClCompile Include="..\src\themes\thememanager.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="fadec.vcxproj">
|
||||
<Project>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="qscintilla.vcxproj">
|
||||
<Project>{F7124B57-7682-4702-B725-4D844DC41ADA}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\third_party\raw_pdb\build\RawPDB.vcxproj">
|
||||
<Project>{fbe3dbfa-20a7-4f99-9326-ed82c8b7b910}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
223
msvc/Reclass.vcxproj.filters
Normal file
223
msvc/Reclass.vcxproj.filters
Normal file
@@ -0,0 +1,223 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\imports">
|
||||
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000001}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\mcp">
|
||||
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000002}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\themes">
|
||||
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000003}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\imports">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000001}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\mcp">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000002}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\providers">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000003}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\themes">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000004}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Form Files">
|
||||
<UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
|
||||
<Extensions>ui</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtRcc Include="..\src\resources.qrc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</QtRcc>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui">
|
||||
<Filter>Form Files</Filter>
|
||||
</QtUic>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\controller.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\editor.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\mainwindow.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\optionsdialog.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\processpicker.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\scanner.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\scannerpanel.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\titlebar.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\typeselectorpopup.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\imports\import_pdb_dialog.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\mcp\mcp_bridge.h">
|
||||
<Filter>Header Files\mcp</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\themes\themeeditor.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\themes\thememanager.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</QtMoc>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\addressparser.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\disasm.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\generator.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\iplugin.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\pluginmanager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providerregistry.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\workspace_model.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\export_reclass_xml.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\import_pdb.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\import_reclass_xml.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\import_source.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\buffer_provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\null_provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\snapshot_provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\themes\theme.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\src\addressparser.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\compose.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\controller.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\disasm.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\editor.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\format.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\generator.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\optionsdialog.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\pluginmanager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\processpicker.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\providerregistry.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\scanner.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\scannerpanel.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\titlebar.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\typeselectorpopup.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\export_reclass_xml.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_pdb.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_reclass_xml.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_source.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\mcp\mcp_bridge.cpp">
|
||||
<Filter>Source Files\mcp</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\themes\theme.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\themes\themeeditor.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\themes\thememanager.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
118
msvc/RemoteProcessMemoryPlugin.vcxproj
Normal file
118
msvc/RemoteProcessMemoryPlugin.vcxproj
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{39E2DDF6-CB76-4063-B957-66ECF1252010}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>RemoteProcessMemoryPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.h" />
|
||||
<ClInclude Include="..\plugins\RemoteProcessMemory\rcx_rpc_protocol.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
112
msvc/WinDbgMemoryPlugin.vcxproj
Normal file
112
msvc/WinDbgMemoryPlugin.vcxproj
Normal file
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{E25D358E-20F0-448B-BB2F-55E9D1F8E7CA}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>WinDbgMemoryPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
81
msvc/fadec.vcxproj
Normal file
81
msvc/fadec.vcxproj
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</ProjectGuid>
|
||||
<RootNamespace>fadec</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<FadecDir>..\third_party\fadec\</FadecDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
|
||||
<LanguageStandard_C>stdc11</LanguageStandard_C>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<LanguageStandard_C>stdc11</LanguageStandard_C>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<Target Name="GenerateFadecTables" BeforeTargets="ClCompile"
|
||||
Inputs="$(FadecDir)instrs.txt;$(FadecDir)parseinstrs.py"
|
||||
Outputs="$(FadecDir)fadec-decode-public.inc;$(FadecDir)fadec-decode-private.inc">
|
||||
<Exec Command="python "$(FadecDir)parseinstrs.py" decode "$(FadecDir)instrs.txt" "$(FadecDir)fadec-decode-public.inc" "$(FadecDir)fadec-decode-private.inc" --32 --64" />
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\fadec\decode.c" />
|
||||
<ClCompile Include="..\third_party\fadec\format.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\third_party\fadec\fadec.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
445
msvc/qscintilla.vcxproj
Normal file
445
msvc/qscintilla.vcxproj
Normal file
@@ -0,0 +1,445 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{F7124B57-7682-4702-B725-4D844DC41ADA}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>qscintilla</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;printsupport</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;printsupport</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<QScintillaDir>..\third_party\qscintilla\</QScintillaDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>TurnOffAllWarnings</WarningLevel>
|
||||
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ExceptionHandling>Sync</ExceptionHandling>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>TurnOffAllWarnings</WarningLevel>
|
||||
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ExceptionHandling>Sync</ExceptionHandling>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- QtMoc headers (Q_OBJECT) — QScintilla Qt wrapper -->
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciabstractapis.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciapis.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexer.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerasm.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeravs.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbash.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbatch.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercmake.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercoffeescript.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercpp.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercsharp.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercss.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercustom.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerd.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerdiff.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeredifact.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran77.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhtml.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeridl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerintelhex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjava.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjavascript.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjson.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerlua.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermakefile.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermarkdown.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermasm.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermatlab.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexernasm.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeroctave.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpascal.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerperl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpostscript.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpo.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpov.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerproperties.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpython.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerruby.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerspice.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersql.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersrec.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertcl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertekhex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerverilog.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexervhdl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerxml.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeryaml.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscimacro.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintilla.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintillabase.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\SciClasses.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\ScintillaQt.h" />
|
||||
</ItemGroup>
|
||||
<!-- ClInclude headers (no Q_OBJECT) -->
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommand.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommandset.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscidocument.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciglobal.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciprinter.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyle.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyledtext.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\ListBoxQt.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\SciAccessibility.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILoader.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Platform.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Sci_Position.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\SciLexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Scintilla.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ScintillaWidget.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexAccessor.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\OptionSet.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SparseState.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StringCopy.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SubStyles.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\WordList.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\AutoComplete.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CallTip.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseConvert.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseFolder.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Catalogue.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CellBuffer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CharClassify.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ContractionState.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\DBCS.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Decoration.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Document.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditModel.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Editor.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditView.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ElapsedPeriod.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\FontQuality.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Indicator.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\IntegerRectangle.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\KeyMap.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\LineMarker.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\MarginView.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Partitioning.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PerLine.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Position.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PositionCache.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RESearch.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RunStyles.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Selection.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SparseVector.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SplitVector.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Style.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniConversion.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniqueString.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ViewStyle.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\XPM.h" />
|
||||
</ItemGroup>
|
||||
<!-- QScintilla Qt wrapper sources -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciscintilla.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciscintillabase.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciabstractapis.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciapis.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscicommand.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscicommandset.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscidocument.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerasm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeravs.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbash.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbatch.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercmake.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercoffeescript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercpp.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercsharp.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercss.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercustom.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerd.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerdiff.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeredifact.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran77.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhtml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeridl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerintelhex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjava.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjavascript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjson.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerlua.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermakefile.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermarkdown.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermasm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermatlab.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexernasm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeroctave.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpascal.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerperl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpostscript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpo.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpov.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerproperties.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpython.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerruby.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerspice.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexersql.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexersrec.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexertcl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexertekhex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexertex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerverilog.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexervhdl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerxml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeryaml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscimacro.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciprinter.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscistyle.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscistyledtext.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\InputMethod.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\ListBoxQt.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\PlatQt.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\SciAccessibility.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\SciClasses.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\ScintillaQt.cpp" />
|
||||
</ItemGroup>
|
||||
<!-- Scintilla lexers -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexA68K.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAPDL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexASY.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAU3.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVE.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAbaqus.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAda.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsn1.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBaan.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBash.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBasic.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBatch.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBibTeX.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBullant.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCLW.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCOBOL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCPP.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCSS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCaml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCmake.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCoffeeScript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexConf.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCrontab.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCsound.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexD.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMAP.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMIS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDiff.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexECL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEDIFACT.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEScript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEiffel.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErlang.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErrorList.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFlagship.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexForth.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFortran.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGAP.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGui4Cli.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHTML.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHaskell.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexIndent.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexInno.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexJSON.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKVIrc.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKix.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLaTeX.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLisp.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLout.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLua.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMMIXAL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMPT.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMSSQL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMagik.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMake.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMarkdown.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMatlab.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMaxima.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMetapost.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexModula.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMySQL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNimrod.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNsis.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNull.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOScript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOpal.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPB.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPLM.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPO.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPOV.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPascal.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPerl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerPro.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerShell.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProgress.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProps.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPython.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexR.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRebol.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRegistry.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRuby.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRust.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSAS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSML.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSQL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSTTXT.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexScriptol.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSmalltalk.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSorcus.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpecman.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpice.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexStata.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTACL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTADS3.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTAL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCMD.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTeX.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTxt2tags.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVB.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVHDL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVerilog.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVisualProlog.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexYAML.cpp" />
|
||||
</ItemGroup>
|
||||
<!-- Scintilla lexlib -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\WordList.cpp" />
|
||||
</ItemGroup>
|
||||
<!-- Scintilla core engine -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\AutoComplete.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CallTip.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseConvert.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseFolder.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Catalogue.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CellBuffer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CharClassify.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ContractionState.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\DBCS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Decoration.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Document.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditModel.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Editor.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditView.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Indicator.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\KeyMap.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\LineMarker.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\MarginView.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PerLine.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PositionCache.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RESearch.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RunStyles.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Selection.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Style.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\UniConversion.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ViewStyle.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\XPM.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
@@ -56,8 +56,13 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
|
||||
m_writable = false;
|
||||
}
|
||||
|
||||
if (m_handle)
|
||||
if (m_handle) {
|
||||
// Detect 32-bit (WoW64) process
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(m_handle, &isWow64) && isWow64)
|
||||
m_pointerSize = 4;
|
||||
cacheModules();
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
@@ -124,6 +129,51 @@ void ProcessMemoryProvider::cacheModules()
|
||||
}
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
if (!m_handle) return regions;
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
uint64_t addr = 0;
|
||||
|
||||
while (VirtualQueryEx(m_handle, (LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi)) {
|
||||
if (mbi.State == MEM_COMMIT &&
|
||||
!(mbi.Protect & PAGE_NOACCESS) &&
|
||||
!(mbi.Protect & PAGE_GUARD))
|
||||
{
|
||||
rcx::MemoryRegion region;
|
||||
region.base = (uint64_t)mbi.BaseAddress;
|
||||
region.size = mbi.RegionSize;
|
||||
region.readable = true;
|
||||
region.writable = (mbi.Protect & PAGE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_WRITECOPY) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READ) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
|
||||
// Match module name from cached module list
|
||||
for (const auto& mod : m_modules) {
|
||||
if (region.base >= mod.base && region.base < mod.base + mod.size) {
|
||||
region.moduleName = mod.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break; // overflow protection
|
||||
addr = next;
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
@@ -147,9 +197,20 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
|
||||
m_writable = false;
|
||||
}
|
||||
|
||||
if (m_fd >= 0)
|
||||
if (m_fd >= 0) {
|
||||
// Detect 32-bit ELF process
|
||||
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
|
||||
QByteArray exePathUtf8 = exePath.toUtf8();
|
||||
int exeFd = ::open(exePathUtf8.constData(), O_RDONLY);
|
||||
if (exeFd >= 0) {
|
||||
unsigned char elfClass = 0;
|
||||
// ELF e_ident[EI_CLASS] is at offset 4
|
||||
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
|
||||
m_pointerSize = 4;
|
||||
::close(exeFd);
|
||||
}
|
||||
cacheModules();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
@@ -282,8 +343,69 @@ void ProcessMemoryProvider::cacheModules()
|
||||
}
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
if (m_fd < 0) return regions;
|
||||
|
||||
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
|
||||
std::ifstream mapsFile(mapsPath.toStdString());
|
||||
if (!mapsFile.is_open()) return regions;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(mapsFile, line)) {
|
||||
std::istringstream iss(line);
|
||||
std::string addrRange, perms, offset, dev, inode, pathname;
|
||||
iss >> addrRange >> perms >> offset >> dev >> inode;
|
||||
std::getline(iss, pathname);
|
||||
|
||||
auto dash = addrRange.find('-');
|
||||
if (dash == std::string::npos) continue;
|
||||
uint64_t addrStart = std::stoull(addrRange.substr(0, dash), nullptr, 16);
|
||||
uint64_t addrEnd = std::stoull(addrRange.substr(dash + 1), nullptr, 16);
|
||||
|
||||
if (perms.size() < 4) continue;
|
||||
bool readable = (perms[0] == 'r');
|
||||
bool writable = (perms[1] == 'w');
|
||||
bool executable = (perms[2] == 'x');
|
||||
|
||||
if (!readable) continue;
|
||||
|
||||
rcx::MemoryRegion region;
|
||||
region.base = addrStart;
|
||||
region.size = addrEnd - addrStart;
|
||||
region.readable = readable;
|
||||
region.writable = writable;
|
||||
region.executable = executable;
|
||||
|
||||
// Extract module name from pathname
|
||||
size_t start = pathname.find_first_not_of(" \t");
|
||||
if (start != std::string::npos) {
|
||||
QString qpath = QString::fromStdString(pathname.substr(start));
|
||||
if (qpath.startsWith('/') && !qpath.startsWith("/dev/") &&
|
||||
!qpath.startsWith("/memfd:")) {
|
||||
QFileInfo fi(qpath);
|
||||
region.moduleName = fi.fileName();
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
#endif // platform
|
||||
|
||||
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
|
||||
@@ -419,6 +541,7 @@ bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
info.name = pinfo.name;
|
||||
info.path = pinfo.path;
|
||||
info.icon = pinfo.icon;
|
||||
info.is32Bit = pinfo.is32Bit;
|
||||
processes.append(info);
|
||||
}
|
||||
|
||||
@@ -480,6 +603,11 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
}
|
||||
}
|
||||
|
||||
// Detect 32-bit (WoW64) process
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(hProcess, &isWow64) && isWow64)
|
||||
info.is32Bit = true;
|
||||
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
|
||||
@@ -526,6 +654,16 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
info.name = procName;
|
||||
info.path = resolvedPath;
|
||||
info.icon = defaultIcon;
|
||||
|
||||
// Detect 32-bit ELF process
|
||||
int exeFd = ::open(exePath.toUtf8().constData(), O_RDONLY);
|
||||
if (exeFd >= 0) {
|
||||
unsigned char elfClass = 0;
|
||||
if (::pread(exeFd, &elfClass, 1, 4) == 1 && elfClass == 1) // ELFCLASS32
|
||||
info.is32Bit = true;
|
||||
::close(exeFd);
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -24,9 +24,12 @@ 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; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
bool isReadable(uint64_t, int len) const override {
|
||||
#ifdef _WIN32
|
||||
return m_handle && len >= 0;
|
||||
@@ -52,6 +55,7 @@ private:
|
||||
QString m_processName;
|
||||
bool m_writable;
|
||||
uint64_t m_base;
|
||||
int m_pointerSize = 8;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
|
||||
@@ -74,6 +74,15 @@ QString RcNetCompatProvider::getSymbol(uint64_t addr) const
|
||||
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 {
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
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;
|
||||
|
||||
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)
|
||||
939
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp
Normal file
939
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp
Normal file
@@ -0,0 +1,939 @@
|
||||
#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;
|
||||
|
||||
RcxRpcHeader* header() const {
|
||||
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
|
||||
}
|
||||
|
||||
~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();
|
||||
// Read pointer size from payload's SHM header (0 means not set → default 8)
|
||||
auto* hdr = m_ipc ? m_ipc->header() : nullptr;
|
||||
if (hdr) {
|
||||
uint32_t ps = hdr->pointerSize;
|
||||
if (ps == 4 || ps == 8)
|
||||
m_pointerSize = (int)ps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
88
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h
Normal file
88
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#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; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
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;
|
||||
int m_pointerSize = 8;
|
||||
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 */
|
||||
115
plugins/RemoteProcessMemory/rcx_rpc_protocol.h
Normal file
115
plugins/RemoteProcessMemory/rcx_rpc_protocol.h
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 pointerSize (4) -- 4 for 32-bit, 8 for 64-bit payload
|
||||
* 52 _pad[4044]
|
||||
*/
|
||||
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) */
|
||||
uint32_t pointerSize; /* 4 for 32-bit, 8 for 64-bit payload */
|
||||
uint8_t _pad[RCX_RPC_HEADER_SIZE - 52];
|
||||
};
|
||||
|
||||
/* ── 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;
|
||||
}
|
||||
@@ -165,6 +165,10 @@ void WinDbgMemoryProvider::initInterfaces()
|
||||
qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_dataSpaces;
|
||||
|
||||
hr = m_client->QueryInterface(IID_IDebugDataSpaces2, (void**)&m_dataSpaces2);
|
||||
qDebug() << "[WinDbg] IDebugDataSpaces2 hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_dataSpaces2;
|
||||
|
||||
hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control);
|
||||
qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_control;
|
||||
@@ -197,53 +201,28 @@ 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);
|
||||
// Query effective processor type for pointer size detection
|
||||
if (m_control) {
|
||||
ULONG procType = 0;
|
||||
hr = m_control->GetEffectiveProcessorType(&procType);
|
||||
if (SUCCEEDED(hr)) {
|
||||
// IMAGE_FILE_MACHINE_I386 = 0x014C
|
||||
if (procType == 0x014C)
|
||||
m_pointerSize = 4;
|
||||
qDebug() << "[WinDbg] EffectiveProcessorType=" << Qt::hex << procType
|
||||
<< "pointerSize=" << m_pointerSize;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -289,10 +268,11 @@ WinDbgMemoryProvider::~WinDbgMemoryProvider()
|
||||
void WinDbgMemoryProvider::cleanup()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
|
||||
if (m_control) { m_control->Release(); m_control = nullptr; }
|
||||
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
|
||||
if (m_client) { m_client->Release(); m_client = nullptr; }
|
||||
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
|
||||
if (m_control) { m_control->Release(); m_control = nullptr; }
|
||||
if (m_dataSpaces2) { m_dataSpaces2->Release(); m_dataSpaces2 = nullptr; }
|
||||
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
|
||||
if (m_client) { m_client->Release(); m_client = nullptr; }
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -305,8 +285,18 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesRead = 0;
|
||||
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
||||
if (FAILED(hr) || (int)bytesRead < len)
|
||||
memset((char*)buf + bytesRead, 0, 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;
|
||||
@@ -379,6 +369,112 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
|
||||
#endif
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> WinDbgMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
#ifdef _WIN32
|
||||
if (!m_dataSpaces) return regions;
|
||||
|
||||
// Enumerate modules — used for tagging (user-mode) or as the primary
|
||||
// source of regions (kernel-mode, where QueryVirtual is unavailable).
|
||||
struct ModInfo { uint64_t base; uint64_t size; QString name; };
|
||||
QVector<ModInfo> modules;
|
||||
|
||||
if (m_symbols) {
|
||||
dispatchToOwner([&]() {
|
||||
ULONG loaded = 0, unloaded = 0;
|
||||
if (FAILED(m_symbols->GetNumberModules(&loaded, &unloaded)))
|
||||
return;
|
||||
for (ULONG i = 0; i < loaded; i++) {
|
||||
ULONG64 modBase = 0;
|
||||
if (FAILED(m_symbols->GetModuleByIndex(i, &modBase)))
|
||||
continue;
|
||||
DEBUG_MODULE_PARAMETERS params = {};
|
||||
if (FAILED(m_symbols->GetModuleParameters(1, &modBase, 0, ¶ms)))
|
||||
continue;
|
||||
char nameBuf[256] = {};
|
||||
ULONG nameSize = 0;
|
||||
m_symbols->GetModuleNames(i, 0,
|
||||
nullptr, 0, nullptr,
|
||||
nameBuf, sizeof(nameBuf), &nameSize,
|
||||
nullptr, 0, nullptr);
|
||||
ModInfo mi;
|
||||
mi.base = modBase;
|
||||
mi.size = params.Size;
|
||||
mi.name = QString::fromUtf8(nameBuf);
|
||||
modules.append(mi);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Try QueryVirtual first (user-mode debugging / user-mode dumps).
|
||||
// MSDN: "This method is not available in kernel-mode debugging."
|
||||
if (m_dataSpaces2) {
|
||||
dispatchToOwner([&]() {
|
||||
ULONG64 addr = 0;
|
||||
int safety = 0;
|
||||
constexpr int kMaxRegions = 500000;
|
||||
|
||||
while (safety++ < kMaxRegions) {
|
||||
MEMORY_BASIC_INFORMATION64 mbi = {};
|
||||
HRESULT hr = m_dataSpaces2->QueryVirtual(addr, &mbi);
|
||||
if (FAILED(hr))
|
||||
break;
|
||||
|
||||
if (mbi.State == MEM_COMMIT &&
|
||||
!(mbi.Protect & PAGE_NOACCESS) &&
|
||||
!(mbi.Protect & PAGE_GUARD))
|
||||
{
|
||||
rcx::MemoryRegion region;
|
||||
region.base = mbi.BaseAddress;
|
||||
region.size = mbi.RegionSize;
|
||||
region.readable = true;
|
||||
region.writable = (mbi.Protect & PAGE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_WRITECOPY) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READ) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
|
||||
for (const auto& mod : modules) {
|
||||
if (region.base >= mod.base && region.base < mod.base + mod.size) {
|
||||
region.moduleName = mod.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
ULONG64 next = mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break;
|
||||
addr = next;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for kernel-mode debugging: QueryVirtual is unavailable,
|
||||
// so use loaded modules as scannable regions. Each module image
|
||||
// becomes one region — the scanner reads through module code/data.
|
||||
if (regions.isEmpty() && !modules.isEmpty()) {
|
||||
for (const auto& mod : modules) {
|
||||
if (mod.size == 0) continue;
|
||||
rcx::MemoryRegion region;
|
||||
region.base = mod.base;
|
||||
region.size = mod.size;
|
||||
region.readable = true;
|
||||
region.writable = false;
|
||||
region.executable = true;
|
||||
region.moduleName = mod.name;
|
||||
regions.append(region);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return regions;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WinDbgMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
@@ -436,7 +532,7 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
QDialog dlg(parent);
|
||||
dlg.setWindowTitle("WinDbg Settings");
|
||||
dlg.resize(460, 260);
|
||||
dlg.resize(460, 300);
|
||||
|
||||
QPalette dlgPal = qApp->palette();
|
||||
dlg.setPalette(dlgPal);
|
||||
@@ -446,7 +542,9 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
|
||||
layout->addWidget(new QLabel(
|
||||
"Connect to a running WinDbg debug server.\n"
|
||||
"In WinDbg, run: .server tcp:port=5055"));
|
||||
"In WinDbg, run: .server tcp:port=5055\n\n"
|
||||
"Non-invasive debug and dump files only.\n"
|
||||
"Execution control (bp, g, t, p) is not supported."));
|
||||
|
||||
layout->addSpacing(8);
|
||||
layout->addWidget(new QLabel("Connection string:"));
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// Forward declarations for DbgEng COM interfaces
|
||||
struct IDebugClient;
|
||||
struct IDebugDataSpaces;
|
||||
struct IDebugDataSpaces2;
|
||||
struct IDebugControl;
|
||||
struct IDebugSymbols;
|
||||
|
||||
@@ -59,9 +60,11 @@ public:
|
||||
QString name() const override { return m_name; }
|
||||
QString kind() const override { return QStringLiteral("WinDbg"); }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
|
||||
bool isLive() const override { return m_isLive; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
|
||||
private:
|
||||
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
||||
@@ -73,16 +76,19 @@ private:
|
||||
template<typename Fn>
|
||||
void dispatchToOwner(Fn&& fn) const;
|
||||
|
||||
IDebugClient* m_client = nullptr;
|
||||
IDebugDataSpaces* m_dataSpaces = nullptr;
|
||||
IDebugControl* m_control = nullptr;
|
||||
IDebugSymbols* m_symbols = nullptr;
|
||||
IDebugClient* m_client = nullptr;
|
||||
IDebugDataSpaces* m_dataSpaces = nullptr;
|
||||
IDebugDataSpaces2* m_dataSpaces2 = nullptr;
|
||||
IDebugControl* m_control = nullptr;
|
||||
IDebugSymbols* m_symbols = nullptr;
|
||||
|
||||
QString m_name;
|
||||
uint64_t m_base = 0;
|
||||
bool m_isLive = false;
|
||||
bool m_writable = false;
|
||||
int m_pointerSize = 8;
|
||||
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
|
||||
|
||||
168
scripts/build_macos.sh
Executable file
168
scripts/build_macos.sh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
print_help() {
|
||||
cat <<'EOF'
|
||||
Reclass macOS Build Script
|
||||
|
||||
Usage:
|
||||
./scripts/build_macos.sh [options]
|
||||
|
||||
Options:
|
||||
--qt-dir <path> Qt installation prefix (e.g. /opt/homebrew/opt/qt)
|
||||
--build-type <type> Release | Debug | RelWithDebInfo | MinSizeRel (default: Release)
|
||||
--build-dir <path> Build directory (default: <repo>/build)
|
||||
--generator <name> CMake generator (default: Ninja if available)
|
||||
--clean Remove build directory before configuring
|
||||
--rebuild Clean then build
|
||||
--package Run macdeployqt and create a zip
|
||||
--tests Run ctest after build
|
||||
-h, --help Show this help
|
||||
|
||||
Notes:
|
||||
- You can set QTDIR or Qt6_DIR in your environment instead of --qt-dir.
|
||||
- If Qt is installed via Homebrew, the script will try to detect it.
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
project_root="$(cd "${script_dir}/.." && pwd)"
|
||||
|
||||
qt_dir=""
|
||||
build_type="Release"
|
||||
build_dir="${project_root}/build"
|
||||
generator=""
|
||||
do_clean="false"
|
||||
do_package="false"
|
||||
do_tests="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--qt-dir)
|
||||
qt_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-type)
|
||||
build_type="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-dir)
|
||||
build_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--generator)
|
||||
generator="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--clean)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--rebuild)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--package)
|
||||
do_package="true"
|
||||
shift
|
||||
;;
|
||||
--tests)
|
||||
do_tests="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
print_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
print_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${qt_dir}" ]]; then
|
||||
if [[ -n "${QTDIR:-}" ]]; then
|
||||
qt_dir="${QTDIR}"
|
||||
elif [[ -n "${Qt6_DIR:-}" ]]; then
|
||||
qt_dir="${Qt6_DIR}"
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
if brew --prefix qt >/dev/null 2>&1; then
|
||||
qt_dir="$(brew --prefix qt)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
echo "ERROR: cmake not found. Install CMake and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${generator}" ]]; then
|
||||
if command -v ninja >/dev/null 2>&1; then
|
||||
generator="Ninja"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${do_clean}" == "true" && -d "${build_dir}" ]]; then
|
||||
echo "Cleaning build directory: ${build_dir}"
|
||||
rm -rf "${build_dir}"
|
||||
fi
|
||||
|
||||
mkdir -p "${build_dir}"
|
||||
|
||||
cmake_args=(
|
||||
-S "${project_root}"
|
||||
-B "${build_dir}"
|
||||
-DCMAKE_BUILD_TYPE="${build_type}"
|
||||
)
|
||||
|
||||
if [[ -n "${generator}" ]]; then
|
||||
cmake_args+=(-G "${generator}")
|
||||
fi
|
||||
|
||||
if [[ -n "${qt_dir}" ]]; then
|
||||
export PATH="${qt_dir}/bin:${PATH}"
|
||||
cmake_args+=(-DCMAKE_PREFIX_PATH="${qt_dir}")
|
||||
fi
|
||||
|
||||
echo "Configuring..."
|
||||
cmake "${cmake_args[@]}"
|
||||
|
||||
echo "Building..."
|
||||
cmake --build "${build_dir}" --config "${build_type}"
|
||||
|
||||
if [[ "${do_tests}" == "true" ]]; then
|
||||
echo "Running tests..."
|
||||
ctest --test-dir "${build_dir}" --output-on-failure -C "${build_type}"
|
||||
fi
|
||||
|
||||
if [[ "${do_package}" == "true" ]]; then
|
||||
app_path="${build_dir}/Reclass.app"
|
||||
if [[ ! -d "${app_path}" ]]; then
|
||||
echo "ERROR: ${app_path} not found. Build may have failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
macdeployqt_bin=""
|
||||
if [[ -n "${qt_dir}" && -x "${qt_dir}/bin/macdeployqt" ]]; then
|
||||
macdeployqt_bin="${qt_dir}/bin/macdeployqt"
|
||||
elif command -v macdeployqt >/dev/null 2>&1; then
|
||||
macdeployqt_bin="$(command -v macdeployqt)"
|
||||
fi
|
||||
|
||||
if [[ -z "${macdeployqt_bin}" ]]; then
|
||||
echo "ERROR: macdeployqt not found. Ensure Qt is installed and in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running macdeployqt..."
|
||||
"${macdeployqt_bin}" "${app_path}" -always-overwrite
|
||||
|
||||
arch="$(uname -m)"
|
||||
zip_name="Reclass-macos-${arch}-qt6.zip"
|
||||
echo "Creating zip: ${zip_name}"
|
||||
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${build_dir}/${zip_name}"
|
||||
echo "Packaged: ${build_dir}/${zip_name}"
|
||||
fi
|
||||
439
src/addressparser.cpp
Normal file
439
src/addressparser.cpp
Normal file
@@ -0,0 +1,439 @@
|
||||
#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)
|
||||
// "base + e_lfanew" → C/C++ style identifier resolution
|
||||
// "0xFF & 0x0F" → bitwise AND
|
||||
// "1 << 4" → shift left
|
||||
//
|
||||
// Grammar (C operator precedence):
|
||||
//
|
||||
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
|
||||
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
|
||||
// bitwiseAnd = shift ('&' shift)*
|
||||
// shift = expr (('<<' | '>>') expr)*
|
||||
// expr = term (('+' | '-') term)*
|
||||
// term = unary (('*' | '/') unary)*
|
||||
// unary = '-' unary | '~' unary | atom
|
||||
// atom = '[' bitwiseOr ']' -- read pointer at address (dereference)
|
||||
// | '<' moduleName '>' -- resolve module base address
|
||||
// | '(' bitwiseOr ')' -- grouping
|
||||
// | identifier -- C/C++ name resolved via callback
|
||||
// | hexLiteral -- hex number, optional 0x prefix
|
||||
//
|
||||
// All numeric literals are hexadecimal (base 16).
|
||||
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
|
||||
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
|
||||
|
||||
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 (!parseBitwiseOr(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');
|
||||
}
|
||||
|
||||
static bool isIdentStart(QChar ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
|
||||
}
|
||||
|
||||
static bool isIdentChar(QChar ch) {
|
||||
return isIdentStart(ch) || (ch >= '0' && ch <= '9');
|
||||
}
|
||||
|
||||
// ── Recursive descent parsing ──
|
||||
|
||||
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
|
||||
bool parseBitwiseOr(uint64_t& result) {
|
||||
if (!parseBitwiseXor(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '|')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseBitwiseXor(rhs))
|
||||
return false;
|
||||
result |= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
|
||||
bool parseBitwiseXor(uint64_t& result) {
|
||||
if (!parseBitwiseAnd(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '^')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseBitwiseAnd(rhs))
|
||||
return false;
|
||||
result ^= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// bitwiseAnd = shift ('&' shift)*
|
||||
bool parseBitwiseAnd(uint64_t& result) {
|
||||
if (!parseShift(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '&')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseShift(rhs))
|
||||
return false;
|
||||
result &= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// shift = expr (('<<' | '>>') expr)*
|
||||
bool parseShift(uint64_t& result) {
|
||||
if (!parseExpression(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar c = peek();
|
||||
if (c != '<' && c != '>')
|
||||
break;
|
||||
// Must be << or >> (not < or > alone)
|
||||
if (m_pos + 1 >= m_input.size() || m_input[m_pos + 1] != c)
|
||||
break;
|
||||
bool isLeft = (c == '<');
|
||||
advance(); advance(); // skip << or >>
|
||||
uint64_t rhs = 0;
|
||||
if (!parseExpression(rhs))
|
||||
return false;
|
||||
result = isLeft ? (result << rhs) : (result >> rhs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 | '~' 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;
|
||||
}
|
||||
if (peek() == '~') {
|
||||
advance();
|
||||
uint64_t inner = 0;
|
||||
if (!parseUnary(inner))
|
||||
return false;
|
||||
result = ~inner;
|
||||
return true;
|
||||
}
|
||||
return parseAtom(result);
|
||||
}
|
||||
|
||||
// atom = '[' bitwiseOr ']' | '<' name '>' | '(' bitwiseOr ')' | identifier | 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);
|
||||
|
||||
// Try identifier before hex — identifiers start with [a-zA-Z_]
|
||||
if (isIdentStart(ch))
|
||||
return parseIdentifierOrHex(result);
|
||||
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// Identifier or hex literal disambiguation.
|
||||
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
|
||||
// Otherwise → backtrack and parse as hex number.
|
||||
bool parseIdentifierOrHex(uint64_t& result) {
|
||||
int start = m_pos;
|
||||
bool hasNonHex = false;
|
||||
|
||||
// Scan full token
|
||||
while (!atEnd() && isIdentChar(peek())) {
|
||||
if (!isHexDigit(peek()))
|
||||
hasNonHex = true;
|
||||
advance();
|
||||
}
|
||||
|
||||
QString token = m_input.mid(start, m_pos - start);
|
||||
|
||||
if (!hasNonHex) {
|
||||
// Pure hex digits (e.g. "DEAD") — backtrack, parse as hex
|
||||
m_pos = start;
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// It's an identifier — resolve via callback
|
||||
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->resolveIdentifier(token, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("unknown identifier '%1'").arg(token));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '[' bitwiseOr ']' — read the pointer value at the computed address
|
||||
bool parseDereference(uint64_t& result) {
|
||||
advance(); // skip '['
|
||||
|
||||
uint64_t address = 0;
|
||||
if (!parseBitwiseOr(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;
|
||||
}
|
||||
|
||||
// '(' bitwiseOr ')' — parenthesized sub-expression for grouping
|
||||
bool parseGrouping(uint64_t& result) {
|
||||
advance(); // skip '('
|
||||
if (!parseBitwiseOr(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)
|
||||
{
|
||||
// ptrSize is used by the caller to configure the readPointer callback;
|
||||
// the parser itself doesn't need it directly.
|
||||
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, dereferences, identifiers succeed but return 0.
|
||||
// This checks syntax only.
|
||||
ExpressionParser parser(cleaned, nullptr);
|
||||
auto result = parser.parse();
|
||||
return result.ok ? QString() : result.error;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
28
src/addressparser.h
Normal file
28
src/addressparser.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#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;
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
|
||||
};
|
||||
|
||||
class AddressParser {
|
||||
public:
|
||||
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
|
||||
const AddressParserCallbacks* cb = nullptr);
|
||||
static QString validate(const QString& formula);
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
414
src/compose.cpp
414
src/compose.cpp
@@ -1,4 +1,5 @@
|
||||
#include "core.h"
|
||||
#include "addressparser.h"
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
@@ -22,6 +23,7 @@ struct ComposeState {
|
||||
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
|
||||
@@ -104,6 +106,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) {
|
||||
@@ -119,10 +128,24 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
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);
|
||||
|
||||
@@ -139,7 +162,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
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;
|
||||
|
||||
@@ -149,7 +172,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||
state.compactColumns);
|
||||
state.emitLine(lineText, lm);
|
||||
}
|
||||
}
|
||||
@@ -239,8 +263,6 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
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) {
|
||||
@@ -251,24 +273,143 @@ 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>& allChildren = childIndices(state, node.id);
|
||||
|
||||
// Split children into regular nodes and static fields (static fields render at the end)
|
||||
QVector<int> regular, staticIdxs;
|
||||
for (int ci : allChildren) {
|
||||
if (tree.nodes[ci].isStatic)
|
||||
staticIdxs.append(ci);
|
||||
else
|
||||
regular.append(ci);
|
||||
}
|
||||
|
||||
int childDepth = depth + 1;
|
||||
|
||||
// Primitive arrays with no child nodes: synthesize element lines dynamically
|
||||
if (node.kind == NodeKind::Array && children.isEmpty()
|
||||
if (node.kind == NodeKind::Array && regular.isEmpty()
|
||||
&& node.elementKind != NodeKind::Struct && node.elementKind != NodeKind::Array) {
|
||||
int elemSize = sizeForKind(node.elementKind);
|
||||
int eTW = state.effectiveTypeW(node.id);
|
||||
@@ -294,22 +435,25 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.nodeKind = node.elementKind;
|
||||
lm.isArrayElement = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Struct arrays with refId but no child nodes: synthesize by expanding the
|
||||
// referenced struct for each element (like repeated pointer deref)
|
||||
if (node.kind == NodeKind::Array && children.isEmpty()
|
||||
if (node.kind == NodeKind::Array && regular.isEmpty()
|
||||
&& node.elementKind == NodeKind::Struct && node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
@@ -326,13 +470,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
|
||||
// Embedded struct with refId but no child nodes: expand referenced struct's
|
||||
// children at this node's offset (single instance, like array with count=1)
|
||||
if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) {
|
||||
if (node.kind == NodeKind::Struct && regular.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) {
|
||||
@@ -341,6 +482,8 @@ 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;
|
||||
@@ -356,10 +499,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
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,
|
||||
@@ -371,7 +514,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
// For arrays, render children as condensed (no header/footer for struct elements)
|
||||
bool childrenAreArrayElements = (node.kind == NodeKind::Array);
|
||||
int elementIdx = 0;
|
||||
for (int childIdx : children) {
|
||||
for (int childIdx : regular) {
|
||||
// Pass this container's id as the scope for children (for per-scope widths)
|
||||
// For array elements, also pass the element index for [N] separator
|
||||
composeNode(state, tree, prov, childIdx, childDepth, base, rootId,
|
||||
@@ -379,6 +522,194 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
childrenAreArrayElements ? elementIdx++ : -1,
|
||||
childrenAreArrayElements ? absAddr : 0);
|
||||
}
|
||||
|
||||
// ── Static fields: render after regular children, before footer ──
|
||||
if (!staticIdxs.isEmpty() && !node.collapsed) {
|
||||
// Build identifier resolver for static field expressions
|
||||
auto makeResolver = [&](uint64_t parentAbsAddr) {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [&tree, &prov, ®ular, parentAbsAddr]
|
||||
(const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == QStringLiteral("base")) {
|
||||
*ok = true;
|
||||
return parentAbsAddr;
|
||||
}
|
||||
// Find sibling field by name, read its value
|
||||
for (int ci : regular) {
|
||||
const Node& sib = tree.nodes[ci];
|
||||
if (sib.name == name) {
|
||||
int sz = sib.byteSize();
|
||||
uint64_t sibAddr = parentAbsAddr + sib.offset;
|
||||
if (sz > 0 && prov.isValid() && prov.isReadable(sibAddr, sz)) {
|
||||
*ok = true;
|
||||
if (sz == 1) return (uint64_t)prov.readU8(sibAddr);
|
||||
if (sz == 2) return (uint64_t)prov.readU16(sibAddr);
|
||||
if (sz == 4) return (uint64_t)prov.readU32(sibAddr);
|
||||
return prov.readU64(sibAddr);
|
||||
}
|
||||
*ok = false;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
*ok = false;
|
||||
return 0;
|
||||
};
|
||||
int ps = tree.pointerSize;
|
||||
cbs.readPointer = [&prov, ps](uint64_t addr, bool* ok) -> uint64_t {
|
||||
if (prov.isValid() && prov.isReadable(addr, ps)) {
|
||||
*ok = true;
|
||||
return (ps >= 8) ? prov.readU64(addr)
|
||||
: (uint64_t)prov.readU32(addr);
|
||||
}
|
||||
*ok = false;
|
||||
return 0;
|
||||
};
|
||||
return cbs;
|
||||
};
|
||||
|
||||
auto cbs = makeResolver(absAddr);
|
||||
|
||||
for (int si : staticIdxs) {
|
||||
const Node& sf = tree.nodes[si];
|
||||
|
||||
// Evaluate expression → absolute address
|
||||
uint64_t staticAddr = 0;
|
||||
bool exprOk = false;
|
||||
if (!sf.offsetExpr.isEmpty()) {
|
||||
auto result = AddressParser::evaluate(sf.offsetExpr, tree.pointerSize, &cbs);
|
||||
exprOk = result.ok;
|
||||
if (result.ok)
|
||||
staticAddr = result.value;
|
||||
}
|
||||
|
||||
// Resolve type name
|
||||
QString typeName;
|
||||
if (sf.kind == NodeKind::Struct)
|
||||
typeName = fmt::structTypeName(sf);
|
||||
else if (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)
|
||||
typeName = fmt::pointerTypeName(sf.kind, resolvePointerTarget(tree, sf.refId));
|
||||
else
|
||||
typeName = fmt::typeNameRaw(sf.kind);
|
||||
|
||||
bool isCollapsed = sf.collapsed;
|
||||
|
||||
// ── Header line: "static <type> <name> {" or collapsed: "static <type> <name> { return <expr>; }"
|
||||
QString headerLine;
|
||||
if (isCollapsed) {
|
||||
QString exprPart;
|
||||
if (!sf.offsetExpr.isEmpty()) {
|
||||
if (exprOk)
|
||||
exprPart = QStringLiteral("return %1 } \u2192 0x%2")
|
||||
.arg(sf.offsetExpr)
|
||||
.arg(QString::number(staticAddr, 16).toUpper());
|
||||
else
|
||||
exprPart = QStringLiteral("return %1 } (error)").arg(sf.offsetExpr);
|
||||
} else {
|
||||
exprPart = QStringLiteral("}");
|
||||
}
|
||||
headerLine = fmt::indent(childDepth)
|
||||
+ QStringLiteral("static ") + typeName
|
||||
+ QStringLiteral(" ") + sf.name
|
||||
+ QStringLiteral(" { ") + exprPart;
|
||||
} else {
|
||||
headerLine = fmt::indent(childDepth)
|
||||
+ QStringLiteral("static ") + typeName
|
||||
+ QStringLiteral(" ") + sf.name
|
||||
+ QStringLiteral(" {");
|
||||
}
|
||||
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = si;
|
||||
lm.nodeId = sf.id;
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.nodeKind = sf.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = isCollapsed;
|
||||
lm.isStaticLine = true;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, true);
|
||||
lm.markerMask = (1u << M_STRUCT_BG);
|
||||
lm.offsetText = QStringLiteral("~") + QString::number(staticAddr, 16)
|
||||
.toUpper().rightJustified(state.offsetHexDigits - 1, '0');
|
||||
lm.offsetAddr = staticAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
|
||||
lm.effectiveNameW = sf.name.size();
|
||||
state.emitLine(headerLine, lm);
|
||||
|
||||
// ── Body + children (only when expanded) ──
|
||||
if (!isCollapsed) {
|
||||
// Body line: " return <expr> → 0xADDR"
|
||||
{
|
||||
QString bodyLine;
|
||||
if (!sf.offsetExpr.isEmpty()) {
|
||||
if (exprOk)
|
||||
bodyLine = fmt::indent(childDepth + 1)
|
||||
+ QStringLiteral("return %1").arg(sf.offsetExpr);
|
||||
else
|
||||
bodyLine = fmt::indent(childDepth + 1)
|
||||
+ QStringLiteral("return %1 (error)").arg(sf.offsetExpr);
|
||||
} else {
|
||||
bodyLine = fmt::indent(childDepth + 1)
|
||||
+ QStringLiteral("return 0");
|
||||
}
|
||||
|
||||
// Right-align resolved address
|
||||
if (exprOk && !sf.offsetExpr.isEmpty()) {
|
||||
bodyLine += QStringLiteral(" \u2192 0x")
|
||||
+ QString::number(staticAddr, 16).toUpper();
|
||||
}
|
||||
|
||||
LineMeta blm;
|
||||
blm.nodeIdx = si;
|
||||
blm.nodeId = sf.id;
|
||||
blm.depth = childDepth + 1;
|
||||
blm.lineKind = LineKind::Field;
|
||||
blm.nodeKind = sf.kind;
|
||||
blm.isStaticLine = true;
|
||||
blm.foldLevel = computeFoldLevel(childDepth + 1, false);
|
||||
blm.markerMask = 0;
|
||||
blm.offsetText = QString(state.offsetHexDigits, QChar(' '));
|
||||
blm.offsetAddr = staticAddr;
|
||||
blm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(bodyLine, blm);
|
||||
}
|
||||
|
||||
// If struct/array, compose children at evaluated address
|
||||
if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
|
||||
const QVector<int>& staticKids = childIndices(state, sf.id);
|
||||
for (int sci : staticKids) {
|
||||
composeNode(state, tree, prov, sci, childDepth + 1,
|
||||
staticAddr, sf.id, false, sf.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer line: "};"
|
||||
{
|
||||
LineMeta flm;
|
||||
flm.nodeIdx = si;
|
||||
flm.nodeId = sf.id;
|
||||
flm.depth = childDepth;
|
||||
flm.lineKind = LineKind::Footer;
|
||||
flm.nodeKind = sf.kind;
|
||||
flm.isStaticLine = true;
|
||||
flm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
flm.markerMask = 0;
|
||||
if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
|
||||
int sSpan = tree.structSpan(sf.id, &state.childMap);
|
||||
flm.offsetText = fmt::fmtOffsetMargin(staticAddr + sSpan, false,
|
||||
state.offsetHexDigits);
|
||||
flm.offsetAddr = staticAddr + sSpan;
|
||||
} else {
|
||||
flm.offsetText = QString(state.offsetHexDigits, QChar(' '));
|
||||
flm.offsetAddr = staticAddr;
|
||||
}
|
||||
flm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer line: skip when collapsed or for array element structs
|
||||
@@ -421,7 +752,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
|
||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||
QVector<int> ptrChildren = state.childMap.value(node.id);
|
||||
const QVector<int>& ptrChildren = childIndices(state, node.id);
|
||||
bool hasMaterialized = !ptrChildren.isEmpty();
|
||||
|
||||
// Force collapsed if this refId is already being virtually expanded
|
||||
@@ -448,12 +779,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
||||
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
||||
lm.effectiveTypeW = typeW;
|
||||
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, effectiveCollapsed,
|
||||
prov, absAddr, ptrTypeOverride,
|
||||
typeW, nameW), lm);
|
||||
typeW, nameW, state.compactColumns), lm);
|
||||
}
|
||||
|
||||
if (!effectiveCollapsed) {
|
||||
@@ -486,9 +818,6 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
// Render materialized children at the pointer target address.
|
||||
// These are real tree nodes with independent state — use rootId
|
||||
// so resolveAddr computes offsets relative to the pointer target.
|
||||
std::sort(ptrChildren.begin(), ptrChildren.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
for (int childIdx : ptrChildren) {
|
||||
composeNode(state, tree, childProv, childIdx, depth + 1,
|
||||
pBase, node.id, false, node.id);
|
||||
@@ -548,13 +877,22 @@ 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);
|
||||
|
||||
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++)
|
||||
@@ -589,11 +927,12 @@ 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
|
||||
@@ -637,7 +976,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);
|
||||
}
|
||||
|
||||
@@ -655,12 +994,12 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
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 NoName {");
|
||||
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE 0x0 struct NoName {");
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = -1;
|
||||
@@ -678,10 +1017,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
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
|
||||
|
||||
1646
src/controller.cpp
1646
src/controller.cpp
File diff suppressed because it is too large
Load Diff
@@ -40,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);
|
||||
@@ -70,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 ──
|
||||
@@ -89,19 +90,28 @@ public:
|
||||
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
||||
void renameNode(int nodeIdx, const QString& newName);
|
||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
|
||||
void removeNode(int nodeIdx);
|
||||
void toggleCollapse(int nodeIdx);
|
||||
void materializeRefChildren(int nodeIdx);
|
||||
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,
|
||||
@@ -117,6 +127,8 @@ public:
|
||||
RcxDocument* document() const { return m_doc; }
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
void setCompactColumns(bool v);
|
||||
void resetProvider();
|
||||
|
||||
// MCP bridge accessors
|
||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||
@@ -124,13 +136,21 @@ 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);
|
||||
void resetChangeTracking();
|
||||
|
||||
// Test accessor
|
||||
// Cross-tab type visibility: point at the project's full document list
|
||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||
|
||||
// Test accessors
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
const ComposeResult& lastResult() const { return m_lastResult; }
|
||||
|
||||
signals:
|
||||
void nodeSelected(int nodeIdx);
|
||||
@@ -143,6 +163,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 ──
|
||||
@@ -151,6 +172,7 @@ private:
|
||||
|
||||
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
||||
QPointer<TypeSelectorPopup> m_cachedPopup;
|
||||
int m_typePopupGen = 0; // generation counter for deferred content loading
|
||||
|
||||
// ── Auto-refresh state ──
|
||||
using PageMap = QHash<uint64_t, QByteArray>;
|
||||
@@ -160,18 +182,19 @@ private:
|
||||
PageMap m_prevPages;
|
||||
QSet<int64_t> m_changedOffsets;
|
||||
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||
bool m_trackValues = false;
|
||||
bool m_trackValues = true;
|
||||
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 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 ──
|
||||
|
||||
249
src/core.h
249
src/core.h
@@ -11,6 +11,7 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
@@ -142,6 +143,15 @@ inline constexpr bool isMatrixKind(NodeKind k) {
|
||||
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;
|
||||
@@ -170,6 +180,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,12 +198,17 @@ struct Node {
|
||||
QString classKeyword; // "struct", "class", or "enum" (empty = "struct")
|
||||
uint64_t parentId = 0; // 0 = root (no parent)
|
||||
int offset = 0;
|
||||
bool isStatic = false; // static field — excluded from struct layout
|
||||
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
|
||||
int arrayLen = 1; // Array: element count
|
||||
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 {
|
||||
@@ -197,6 +220,12 @@ struct Node {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -212,11 +241,38 @@ struct Node {
|
||||
o["classKeyword"] = classKeyword;
|
||||
o["parentId"] = QString::number(parentId);
|
||||
o["offset"] = offset;
|
||||
if (isStatic)
|
||||
o["isStatic"] = true;
|
||||
if (!offsetExpr.isEmpty())
|
||||
o["offsetExpr"] = offsetExpr;
|
||||
o["arrayLen"] = arrayLen;
|
||||
o["strLen"] = strLen;
|
||||
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) {
|
||||
@@ -228,11 +284,33 @@ struct Node {
|
||||
n.classKeyword = o["classKeyword"].toString();
|
||||
n.parentId = o["parentId"].toString("0").toULongLong();
|
||||
n.offset = o["offset"].toInt(0);
|
||||
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
|
||||
n.offsetExpr = o["offsetExpr"].toString();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -254,6 +332,8 @@ struct Node {
|
||||
struct NodeTree {
|
||||
QVector<Node> nodes;
|
||||
uint64_t baseAddress = 0x00400000;
|
||||
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
||||
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
||||
uint64_t m_nextId = 1;
|
||||
mutable QHash<uint64_t, int> m_idCache;
|
||||
|
||||
@@ -367,6 +447,7 @@ struct NodeTree {
|
||||
QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId);
|
||||
for (int ci : kids) {
|
||||
const Node& c = nodes[ci];
|
||||
if (c.isStatic) continue; // static fields don't affect struct size
|
||||
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
|
||||
? structSpan(c.id, childMap, visited) : c.byteSize();
|
||||
int end = c.offset + sz;
|
||||
@@ -387,6 +468,10 @@ struct NodeTree {
|
||||
QJsonObject toJson() const {
|
||||
QJsonObject o;
|
||||
o["baseAddress"] = QString::number(baseAddress, 16);
|
||||
if (!baseAddressFormula.isEmpty())
|
||||
o["baseAddressFormula"] = baseAddressFormula;
|
||||
if (pointerSize != 8)
|
||||
o["pointerSize"] = pointerSize;
|
||||
o["nextId"] = QString::number(m_nextId);
|
||||
QJsonArray arr;
|
||||
for (const auto& n : nodes) arr.append(n.toJson());
|
||||
@@ -397,6 +482,8 @@ 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.pointerSize = o["pointerSize"].toInt(8);
|
||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||
QJsonArray arr = o["nodes"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
@@ -414,6 +501,7 @@ struct NodeTree {
|
||||
struct ValueHistory {
|
||||
static constexpr int kCapacity = 10;
|
||||
std::array<QString, kCapacity> values;
|
||||
std::array<qint64, kCapacity> timestamps{}; // msec since epoch
|
||||
int count = 0; // total unique values recorded
|
||||
int head = 0; // next write position in ring
|
||||
|
||||
@@ -423,10 +511,16 @@ struct ValueHistory {
|
||||
if (values[last] == v) return; // no change
|
||||
}
|
||||
values[head] = v;
|
||||
timestamps[head] = QDateTime::currentMSecsSinceEpoch();
|
||||
head = (head + 1) % kCapacity;
|
||||
if (count < INT_MAX) count++;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
count = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||
|
||||
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||
@@ -450,6 +544,16 @@ struct ValueHistory {
|
||||
for (int i = 0; i < n; i++)
|
||||
fn(values[(start + i) % kCapacity]);
|
||||
}
|
||||
|
||||
// Iterate with timestamps from newest to oldest
|
||||
template<typename Fn>
|
||||
void forEachWithTime(Fn&& fn) const {
|
||||
int n = uniqueCount();
|
||||
for (int i = 0; i < n; i++) {
|
||||
int idx = (head + kCapacity - 1 - i) % kCapacity;
|
||||
fn(values[idx], timestamps[idx]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── LineMeta ──
|
||||
@@ -464,6 +568,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;
|
||||
@@ -494,6 +621,8 @@ struct LineMeta {
|
||||
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
|
||||
bool isStaticLine = false; // true for static field node lines
|
||||
};
|
||||
|
||||
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
@@ -528,7 +657,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;
|
||||
@@ -538,13 +667,18 @@ 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; };
|
||||
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
|
||||
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
|
||||
}
|
||||
|
||||
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,
|
||||
cmd::ChangeOffsetExpr, cmd::ToggleStatic
|
||||
>;
|
||||
|
||||
// ── Column spans (for inline editing) ──
|
||||
@@ -557,7 +691,7 @@ struct ColumnSpan {
|
||||
|
||||
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
|
||||
ArrayElementType, ArrayElementCount, PointerTarget,
|
||||
RootClassType, RootClassName, TypeSelector };
|
||||
RootClassType, RootClassName, TypeSelector, StaticExpr };
|
||||
|
||||
// Column layout constants (shared with format.cpp span computation)
|
||||
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
||||
@@ -571,15 +705,16 @@ 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;
|
||||
@@ -594,6 +729,7 @@ 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 uses nameW for ASCII column (same as regular name column)
|
||||
@@ -612,6 +748,45 @@ 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};
|
||||
}
|
||||
|
||||
// Static field expression span: locates text between "return " and "→" / "(error)" / end
|
||||
inline ColumnSpan staticExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) {
|
||||
int ret = lineText.indexOf(QLatin1String("return "));
|
||||
if (ret < 0) return {};
|
||||
int exprStart = ret + 7;
|
||||
// End: before arrow, before "(error)", or line end
|
||||
int exprEnd = lineText.size();
|
||||
int arrow = lineText.indexOf(QChar(0x2192), exprStart);
|
||||
if (arrow > exprStart) exprEnd = arrow;
|
||||
int err = lineText.indexOf(QLatin1String("(error)"), exprStart);
|
||||
if (err > exprStart && err < exprEnd) exprEnd = err;
|
||||
// Also stop at " }" for collapsed format
|
||||
int brace = lineText.indexOf(QLatin1String(" }"), exprStart);
|
||||
if (brace > exprStart && brace < exprEnd) exprEnd = brace;
|
||||
while (exprEnd > exprStart && lineText[exprEnd - 1] == ' ') exprEnd--;
|
||||
return {exprStart, exprEnd, 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;
|
||||
@@ -633,27 +808,14 @@ 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 ──
|
||||
@@ -672,6 +834,25 @@ inline int commandRowRootStart(const QString& lineText) {
|
||||
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 {};
|
||||
@@ -820,17 +1001,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,
|
||||
@@ -840,10 +1022,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
|
||||
|
||||
468
src/editor.cpp
468
src/editor.cpp
@@ -19,8 +19,10 @@
|
||||
#include <QClipboard>
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
#include <QLineEdit>
|
||||
#include <QScreen>
|
||||
#include <QScrollBar>
|
||||
#include <QDateTime>
|
||||
#include <functional>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
@@ -102,7 +104,8 @@ public:
|
||||
sep->setPalette(sp);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
for (const QString& v : vals) {
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
hist.forEachWithTime([&](const QString& v, qint64 msec) {
|
||||
auto* row = new QHBoxLayout;
|
||||
row->setContentsMargins(0, 1, 0, 1);
|
||||
row->setSpacing(8);
|
||||
@@ -113,6 +116,24 @@ public:
|
||||
row->addWidget(label, 1);
|
||||
m_labels.append(label);
|
||||
|
||||
if (msec > 0) {
|
||||
qint64 elapsed = now - msec;
|
||||
QString timeStr;
|
||||
if (elapsed < 1000)
|
||||
timeStr = QStringLiteral("now");
|
||||
else if (elapsed < 60000)
|
||||
timeStr = QStringLiteral("%1s ago").arg(elapsed / 1000);
|
||||
else if (elapsed < 3600000)
|
||||
timeStr = QStringLiteral("%1m ago").arg(elapsed / 60000);
|
||||
else
|
||||
timeStr = QStringLiteral("%1h ago").arg(elapsed / 3600000);
|
||||
|
||||
auto* timeLabel = new QLabel(timeStr);
|
||||
timeLabel->setFont(font);
|
||||
timeLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
||||
row->addWidget(timeLabel);
|
||||
}
|
||||
|
||||
if (showButtons) {
|
||||
auto* setBtn = new QToolButton;
|
||||
setBtn->setText(QStringLiteral("Set"));
|
||||
@@ -130,7 +151,7 @@ public:
|
||||
row->addWidget(setBtn);
|
||||
}
|
||||
vbox->addLayout(row);
|
||||
}
|
||||
});
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
@@ -364,6 +385,7 @@ static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text
|
||||
static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode
|
||||
static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes)
|
||||
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes)
|
||||
static constexpr int IND_FIND = 19; // Search match highlight
|
||||
|
||||
static QString g_fontName = "JetBrains Mono";
|
||||
|
||||
@@ -380,6 +402,29 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
m_sci = new QsciScintilla(this);
|
||||
layout->addWidget(m_sci);
|
||||
|
||||
// Find bar (hidden by default, shown with Ctrl+F)
|
||||
m_findBarContainer = new QWidget(this);
|
||||
auto* fbLayout = new QHBoxLayout(m_findBarContainer);
|
||||
fbLayout->setContentsMargins(4, 0, 0, 0);
|
||||
fbLayout->setSpacing(2);
|
||||
auto* findPrevBtn = new QToolButton(m_findBarContainer);
|
||||
findPrevBtn->setText(QStringLiteral("\u25C0"));
|
||||
findPrevBtn->setFixedSize(24, 24);
|
||||
auto* findNextBtn = new QToolButton(m_findBarContainer);
|
||||
findNextBtn->setText(QStringLiteral("\u25B6"));
|
||||
findNextBtn->setFixedSize(24, 24);
|
||||
auto* findCloseBtn = new QToolButton(m_findBarContainer);
|
||||
findCloseBtn->setText(QStringLiteral("\u2715"));
|
||||
findCloseBtn->setFixedSize(24, 24);
|
||||
m_findBar = new QLineEdit(m_findBarContainer);
|
||||
m_findBar->setPlaceholderText(QStringLiteral("Find..."));
|
||||
fbLayout->addWidget(findPrevBtn);
|
||||
fbLayout->addWidget(findNextBtn);
|
||||
fbLayout->addWidget(findCloseBtn);
|
||||
fbLayout->addWidget(m_findBar);
|
||||
m_findBarContainer->setVisible(false);
|
||||
layout->addWidget(m_findBarContainer);
|
||||
|
||||
setupScintilla();
|
||||
setupLexer();
|
||||
setupMargins();
|
||||
@@ -395,6 +440,55 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
m_sci->viewport()->installEventFilter(this);
|
||||
m_sci->viewport()->setMouseTracking(true);
|
||||
|
||||
// Find bar: indicator-based search (selection is disabled in our Scintilla)
|
||||
auto doFind = [this](bool forward) {
|
||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (long)0, docLen);
|
||||
|
||||
QString text = m_findBar->text();
|
||||
if (text.isEmpty()) return;
|
||||
QByteArray needle = text.toUtf8();
|
||||
|
||||
long startPos = forward ? m_findPos : (m_findPos > 0 ? m_findPos - 1 : docLen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, startPos);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND,
|
||||
forward ? docLen : (long)0);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEARCHFLAGS, (long)0);
|
||||
|
||||
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
||||
(uintptr_t)needle.size(), needle.constData());
|
||||
if (pos == -1) { // wrap
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART,
|
||||
forward ? (long)0 : docLen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND,
|
||||
forward ? startPos : (long)0);
|
||||
pos = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
||||
(uintptr_t)needle.size(), needle.constData());
|
||||
}
|
||||
if (pos >= 0) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, (long)needle.size());
|
||||
int line = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_LINEFROMPOSITION, pos);
|
||||
m_sci->ensureLineVisible(line);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, pos);
|
||||
m_findPos = pos + (forward ? needle.size() : 0);
|
||||
}
|
||||
};
|
||||
connect(m_findBar, &QLineEdit::textChanged, this, [doFind]() { doFind(true); });
|
||||
connect(m_findBar, &QLineEdit::returnPressed, this, [doFind]() { doFind(true); });
|
||||
connect(findNextBtn, &QToolButton::clicked, this, [doFind]() { doFind(true); });
|
||||
connect(findPrevBtn, &QToolButton::clicked, this, [doFind]() { doFind(false); });
|
||||
connect(findCloseBtn, &QToolButton::clicked, this, [this]() { hideFindBar(); });
|
||||
// Escape hides find bar
|
||||
{
|
||||
auto* escAction = new QAction(m_findBar);
|
||||
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
|
||||
escAction->setShortcutContext(Qt::WidgetShortcut);
|
||||
m_findBar->addAction(escAction);
|
||||
connect(escAction, &QAction::triggered, this, [this]() { hideFindBar(); });
|
||||
}
|
||||
|
||||
// Recalculate hover when the viewport scrolls (scrollbar drag, wheel
|
||||
// deceleration, etc.) so the highlight tracks whatever is under the cursor.
|
||||
connect(m_sci->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
@@ -488,8 +582,10 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
if (id == 1 && (m_editState.target == EditTarget::Type
|
||||
|| m_editState.target == EditTarget::ArrayElementType
|
||||
|| m_editState.target == EditTarget::PointerTarget)) {
|
||||
const LineMeta* lm = metaForLine(m_editState.line);
|
||||
uint64_t addr = lm ? lm->offsetAddr : 0;
|
||||
auto info = endInlineEdit();
|
||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
|
||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -501,6 +597,19 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
if (m_updatingComment) return; // Skip queuing during comment update
|
||||
if (m_editState.target == EditTarget::Value)
|
||||
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
||||
|
||||
// Autocomplete for static field expressions — show field names as user types
|
||||
if (m_editState.target == EditTarget::StaticExpr && !m_staticCompletions.isEmpty()) {
|
||||
// Get word at cursor
|
||||
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
||||
long wordStart = m_sci->SendScintilla(QsciScintillaBase::SCI_WORDSTARTPOSITION, pos, (long)1);
|
||||
int wordLen = (int)(pos - wordStart);
|
||||
if (wordLen >= 1) {
|
||||
QByteArray list = m_staticCompletions.join(' ').toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW, (uintptr_t)wordLen, list.constData());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_sci, &QsciScintilla::selectionChanged,
|
||||
@@ -583,6 +692,12 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/);
|
||||
|
||||
// Find match highlight — thick underline (avoids box rendering artifacts)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_FIND, 14 /*INDIC_COMPOSITIONTHICK*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||
IND_FIND, (long)1);
|
||||
|
||||
}
|
||||
|
||||
void RcxEditor::setupLexer() {
|
||||
@@ -719,6 +834,8 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
IND_HINT_GREEN, theme.indHintGreen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_LOCAL_OFF, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_FIND, theme.borderFocused);
|
||||
|
||||
// Lexer colors
|
||||
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
|
||||
@@ -745,8 +862,8 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
// Markers
|
||||
m_sci->setMarkerBackgroundColor(theme.markerPtr, M_PTR0);
|
||||
m_sci->setMarkerForegroundColor(theme.markerPtr, M_PTR0);
|
||||
m_sci->setMarkerBackgroundColor(theme.markerCycle, M_CYCLE);
|
||||
m_sci->setMarkerForegroundColor(theme.markerCycle, M_CYCLE);
|
||||
m_sci->setMarkerBackgroundColor(theme.background, M_CYCLE);
|
||||
m_sci->setMarkerForegroundColor(theme.background, M_CYCLE);
|
||||
m_sci->setMarkerBackgroundColor(theme.markerError, M_ERR);
|
||||
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
|
||||
m_sci->setMarkerBackgroundColor(theme.background, M_STRUCT_BG);
|
||||
@@ -767,6 +884,20 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
abs, theme.background);
|
||||
}
|
||||
}
|
||||
|
||||
// Find bar
|
||||
if (m_findBarContainer) {
|
||||
m_findBar->setStyleSheet(
|
||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||
" padding: 4px 8px; font-size: 13px; }")
|
||||
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name()));
|
||||
m_findBarContainer->setStyleSheet(
|
||||
QStringLiteral("QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
|
||||
"QToolButton:hover { background: %4; }"
|
||||
"QToolButton:pressed { background: %5; }")
|
||||
.arg(theme.background.name(), theme.text.name(), theme.border.name(),
|
||||
theme.hover.name(), theme.backgroundAlt.name()));
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
@@ -785,6 +916,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
m_meta = result.meta;
|
||||
m_layout = result.layout;
|
||||
|
||||
// Build nodeId → display-line index for O(1) hover/selection lookup
|
||||
m_nodeLineIndex.clear();
|
||||
m_nodeLineIndex.reserve(m_meta.size());
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId != 0)
|
||||
m_nodeLineIndex[m_meta[i].nodeId].append(i);
|
||||
}
|
||||
|
||||
// Dynamically resize margin to fit the current hex digit tier
|
||||
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
||||
m_sci->setMarginWidth(0, marginSizer);
|
||||
@@ -806,6 +945,10 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH,
|
||||
(unsigned long)qMax(1, pixelWidth));
|
||||
|
||||
// Reset horizontal scroll to 0. The controller's restoreViewState()
|
||||
// will set it back to the (clamped) saved position afterward.
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)0);
|
||||
}
|
||||
|
||||
// Force full re-lex to fix stale syntax coloring after edits
|
||||
@@ -829,10 +972,34 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
m_applyingDocument = false;
|
||||
|
||||
// Re-apply hover markers (setText() clears all Scintilla markers).
|
||||
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
|
||||
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
||||
// composed text that updateCommandRow() will overwrite. The correct call
|
||||
// happens via applySelectionOverlays() after all text is finalized.
|
||||
m_prevHoveredNodeId = 0;
|
||||
m_prevHoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
|
||||
// Re-apply find indicator (setText() clears all indicators)
|
||||
if (m_findBarContainer && m_findBarContainer->isVisible()) {
|
||||
QString needle = m_findBar->text();
|
||||
if (!needle.isEmpty()) {
|
||||
QByteArray nb = needle.toUtf8();
|
||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEARCHFLAGS, (long)0);
|
||||
long pos = 0;
|
||||
while (pos < docLen) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, pos);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, docLen);
|
||||
long found = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
||||
(uintptr_t)nb.size(), nb.constData());
|
||||
if (found < 0) break;
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, found, (long)nb.size());
|
||||
pos = found + nb.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
||||
@@ -863,7 +1030,7 @@ void RcxEditor::reformatMargins() {
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
auto& lm = m_meta[i];
|
||||
|
||||
if (lm.isContinuation) {
|
||||
if (lm.isContinuation || lm.isMemberLine) {
|
||||
lm.offsetText = QStringLiteral(" \u00B7 ");
|
||||
} else if (lm.offsetText.isEmpty()) {
|
||||
continue;
|
||||
@@ -903,7 +1070,7 @@ void RcxEditor::reformatMargins() {
|
||||
// Place offset in the parent's indent slot (one level above the field's own indent)
|
||||
// so the field's own 3-char indent acts as visual separator from the type column
|
||||
int col = kFoldCol + (lm.depth - 2) * 3;
|
||||
int slotWidth = 3;
|
||||
int slotWidth = 5;
|
||||
|
||||
auto pos = [&](int c) -> long {
|
||||
return m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||
@@ -1058,18 +1225,41 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
||||
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (isSyntheticLine(m_meta[i])) continue;
|
||||
uint64_t nodeId = m_meta[i].nodeId;
|
||||
bool isFooter = (m_meta[i].lineKind == LineKind::Footer);
|
||||
|
||||
// Footers check for footerId, non-footers check for plain nodeId
|
||||
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
|
||||
if (selIds.contains(checkId)) {
|
||||
m_sci->markerAdd(i, M_SELECTED);
|
||||
m_sci->markerAdd(i, M_ACCENT);
|
||||
// Use index: iterate selected IDs, look up their lines
|
||||
for (uint64_t selId : selIds) {
|
||||
bool isFooterSel = (selId & kFooterIdBit) != 0;
|
||||
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
|
||||
bool isMemberSel = (selId & kMemberBit) != 0;
|
||||
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
|
||||
int memberSubLine = isMemberSel ? memberSubFromSelId(selId) : -1;
|
||||
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||
| kMemberBit | kMemberSubMask);
|
||||
auto it = m_nodeLineIndex.constFind(nodeId);
|
||||
if (it == m_nodeLineIndex.constEnd()) continue;
|
||||
for (int ln : *it) {
|
||||
if (isSyntheticLine(m_meta[ln])) continue;
|
||||
bool isFooter = (m_meta[ln].lineKind == LineKind::Footer);
|
||||
// Match selection type to line type
|
||||
if (isFooterSel && !isFooter) continue;
|
||||
if (!isFooterSel && isFooter) continue;
|
||||
// Array element: match by element index
|
||||
if (isArrayElemSel) {
|
||||
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
|
||||
continue;
|
||||
} else if (m_meta[ln].isArrayElement) {
|
||||
continue;
|
||||
}
|
||||
// Member line: match by subLine index
|
||||
if (isMemberSel) {
|
||||
if (!m_meta[ln].isMemberLine || m_meta[ln].subLine != memberSubLine)
|
||||
continue;
|
||||
} else if (m_meta[ln].isMemberLine) {
|
||||
continue;
|
||||
}
|
||||
m_sci->markerAdd(ln, M_SELECTED);
|
||||
m_sci->markerAdd(ln, M_ACCENT);
|
||||
if (!isFooter)
|
||||
paintEditableSpans(i);
|
||||
paintEditableSpans(ln);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,28 +1272,68 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
||||
}
|
||||
|
||||
void RcxEditor::applyHoverHighlight() {
|
||||
m_sci->markerDeleteAll(M_HOVER);
|
||||
uint64_t prevId = m_prevHoveredNodeId;
|
||||
int prevLine = m_prevHoveredLine;
|
||||
m_prevHoveredNodeId = m_hoveredNodeId;
|
||||
m_prevHoveredLine = m_hoveredLine;
|
||||
|
||||
// Fast path: nothing changed (same node AND same line)
|
||||
if (prevId == m_hoveredNodeId && prevLine == m_hoveredLine
|
||||
&& m_hoveredNodeId != 0) return;
|
||||
|
||||
// Remove old hover markers
|
||||
if (prevId != 0) {
|
||||
// Check if old hovered line was a single-line highlight (footer or array element)
|
||||
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
|
||||
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement
|
||||
|| m_meta[prevLine].isMemberLine));
|
||||
if (prevSingleLine) {
|
||||
m_sci->markerDelete(prevLine, M_HOVER);
|
||||
} else {
|
||||
auto it = m_nodeLineIndex.constFind(prevId);
|
||||
if (it != m_nodeLineIndex.constEnd()) {
|
||||
for (int ln : *it)
|
||||
m_sci->markerDelete(ln, M_HOVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_editState.active) return;
|
||||
if (!m_hoverInside) return;
|
||||
if (m_hoveredNodeId == 0) return;
|
||||
|
||||
// Check if hovered line is a footer - footers highlight independently
|
||||
// Footer, array elements, and member lines highlight only the specific line
|
||||
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
||||
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].isArrayElement);
|
||||
bool hoveringMember = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].isMemberLine);
|
||||
|
||||
// Check if the hovered item is already selected (using appropriate ID)
|
||||
uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId;
|
||||
uint64_t checkId;
|
||||
if (hoveringFooter)
|
||||
checkId = m_hoveredNodeId | kFooterIdBit;
|
||||
else if (hoveringArrayElem)
|
||||
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
|
||||
else if (hoveringMember)
|
||||
checkId = makeMemberSelId(m_hoveredNodeId, m_meta[m_hoveredLine].subLine);
|
||||
else
|
||||
checkId = m_hoveredNodeId;
|
||||
if (m_currentSelIds.contains(checkId)) return;
|
||||
|
||||
if (hoveringFooter) {
|
||||
// Footer: only highlight this specific line
|
||||
if (hoveringFooter || hoveringArrayElem || hoveringMember) {
|
||||
// Single-line highlight for footers, array elements, and member lines
|
||||
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
||||
} else {
|
||||
// Non-footer: highlight all matching lines except footers
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId == m_hoveredNodeId &&
|
||||
m_meta[i].lineKind != LineKind::Footer)
|
||||
m_sci->markerAdd(i, M_HOVER);
|
||||
// Non-footer, non-array-element: highlight all lines for this node
|
||||
auto it = m_nodeLineIndex.constFind(m_hoveredNodeId);
|
||||
if (it != m_nodeLineIndex.constEnd()) {
|
||||
for (int ln : *it) {
|
||||
if (m_meta[ln].lineKind != LineKind::Footer &&
|
||||
!m_meta[ln].isArrayElement)
|
||||
m_sci->markerAdd(ln, M_HOVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1128,8 +1358,13 @@ void RcxEditor::restoreViewState(const ViewState& vs) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, (unsigned long)pos);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
|
||||
(unsigned long)vs.scrollLine);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET,
|
||||
(unsigned long)vs.xOffset);
|
||||
// Clamp xOffset so it doesn't exceed the current content width.
|
||||
// After a rename that shrinks content, the saved offset may be stale.
|
||||
int scrollW = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
|
||||
int vpW = m_sci->viewport() ? m_sci->viewport()->width() : 0;
|
||||
int maxXOff = qMax(0, scrollW - vpW);
|
||||
int xOff = qBound(0, vs.xOffset, maxXOff);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)xOff);
|
||||
}
|
||||
|
||||
const LineMeta* RcxEditor::metaForLine(int line) const {
|
||||
@@ -1145,6 +1380,27 @@ int RcxEditor::currentNodeIndex() const {
|
||||
return lm ? lm->nodeIdx : -1;
|
||||
}
|
||||
|
||||
void RcxEditor::showFindBar() {
|
||||
m_findBarContainer->setVisible(true);
|
||||
m_findBar->setFocus();
|
||||
m_findBar->selectAll();
|
||||
m_findPos = 0;
|
||||
}
|
||||
|
||||
void RcxEditor::dismissHistoryPopup() {
|
||||
if (m_historyPopup)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
|
||||
void RcxEditor::hideFindBar() {
|
||||
m_findBarContainer->setVisible(false);
|
||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (long)0, docLen);
|
||||
m_findPos = 0;
|
||||
m_sci->setFocus();
|
||||
}
|
||||
|
||||
void RcxEditor::scrollToNodeId(uint64_t nodeId) {
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId == nodeId && m_meta[i].lineKind != LineKind::Footer) {
|
||||
@@ -1220,7 +1476,12 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
||||
int typeW = lm.effectiveTypeW;
|
||||
int nameW = lm.effectiveNameW;
|
||||
|
||||
if (heat <= 0) continue;
|
||||
if (heat <= 0) {
|
||||
// Clear any stale heat indicators from a previous frame
|
||||
for (int hi : heatIndicators)
|
||||
clearIndicatorLine(hi, i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||
@@ -1302,15 +1563,6 @@ void RcxEditor::applyCommandRowPills() {
|
||||
if (srcDrop >= 0 && (rootStart < 0 || srcDrop < rootStart))
|
||||
fillIndicatorCols(IND_HEX_DIM, line, srcDrop, srcDrop + 1);
|
||||
}
|
||||
// Dim all " · " separators
|
||||
int searchFrom = 0;
|
||||
while (true) {
|
||||
int tag = t.indexOf(QStringLiteral(" \u00B7"), searchFrom);
|
||||
if (tag < 0) break;
|
||||
fillIndicatorCols(IND_HEX_DIM, line, tag, tag + 3);
|
||||
searchFrom = tag + 3;
|
||||
}
|
||||
|
||||
// Dim base address to match source/struct grey
|
||||
ColumnSpan addrSpan = commandRowAddrSpan(t);
|
||||
if (addrSpan.valid)
|
||||
@@ -1392,39 +1644,49 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
|
||||
}
|
||||
|
||||
// Type name span for struct headers (not arrays)
|
||||
// Format: "struct TYPENAME NAME {" or collapsed variants
|
||||
// For "struct NAME {" (no typename), returns invalid span
|
||||
// Named structs format as: "_MMPTE OriginalPte {" (type column = just the name)
|
||||
// Anonymous structs format as: "union {" or "struct {" (no clickable type)
|
||||
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
|
||||
if (lm.lineKind != LineKind::Header) return {};
|
||||
if (lm.isArrayHeader) return {}; // Arrays use arrayHeaderTypeSpan instead
|
||||
if (lm.isArrayHeader) return {};
|
||||
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int typeW = lm.effectiveTypeW;
|
||||
int typeEnd = ind + typeW;
|
||||
|
||||
// Clamp to actual line content
|
||||
if (typeEnd > lineText.size()) typeEnd = lineText.size();
|
||||
|
||||
// Extract the type column text and check if it has a typename
|
||||
// Format: "struct" or "struct TYPENAME"
|
||||
QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed();
|
||||
if (typeCol.isEmpty()) return {};
|
||||
|
||||
// Find first space (after "struct")
|
||||
int firstSpace = typeCol.indexOf(' ');
|
||||
if (firstSpace < 0) return {}; // Just "struct", no typename
|
||||
// Anonymous structs use bare keywords — not clickable
|
||||
static const QStringList kKeywords = {
|
||||
QStringLiteral("struct"), QStringLiteral("union"), QStringLiteral("class")
|
||||
};
|
||||
if (kKeywords.contains(typeCol)) return {};
|
||||
|
||||
// If there's content after "struct ", that's the typename
|
||||
QString typename_ = typeCol.mid(firstSpace + 1).trimmed();
|
||||
if (typename_.isEmpty()) return {};
|
||||
// Static field headers: "static hex64 target {" — skip "static " prefix
|
||||
if (lm.isStaticLine) {
|
||||
int cursor = ind;
|
||||
while (cursor < typeEnd && lineText[cursor] == ' ') cursor++;
|
||||
if (lineText.mid(cursor, 7) == QLatin1String("static "))
|
||||
cursor += 7;
|
||||
while (cursor < typeEnd && lineText[cursor] == ' ') cursor++;
|
||||
int tStart = cursor;
|
||||
while (cursor < typeEnd && lineText[cursor] != ' ') cursor++;
|
||||
if (cursor > tStart)
|
||||
return {tStart, cursor, true};
|
||||
return {};
|
||||
}
|
||||
|
||||
// Return span of the typename within the type column
|
||||
int typenameStart = ind + firstSpace + 1;
|
||||
// Find where the typename actually ends (skip padding)
|
||||
int typenameEnd = typenameStart;
|
||||
while (typenameEnd < typeEnd && lineText[typenameEnd] != ' ')
|
||||
typenameEnd++;
|
||||
// Named struct: entire type column is the type name (e.g. "_MMPTE")
|
||||
// Find the actual text bounds within the padded column
|
||||
int start = ind;
|
||||
while (start < typeEnd && lineText[start] == ' ') start++;
|
||||
int end = start;
|
||||
while (end < typeEnd && lineText[end] != ' ') end++;
|
||||
if (end <= start) return {};
|
||||
|
||||
return {typenameStart, typenameEnd, true};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
|
||||
@@ -1501,7 +1763,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
if (lm->nodeIdx < 0) return false;
|
||||
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||
// Exception: static field names are always editable (they're function names)
|
||||
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine)
|
||||
return false;
|
||||
|
||||
QString lineText = getLineText(m_sci, line);
|
||||
@@ -1515,7 +1778,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
switch (t) {
|
||||
case EditTarget::Type: s = typeSpan(*lm, typeW); break;
|
||||
case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
|
||||
case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break;
|
||||
case EditTarget::Value: s = narrowPtrValueSpan(*lm,
|
||||
valueSpan(*lm, textLen, typeW, nameW), lineText); break;
|
||||
case EditTarget::BaseAddress: break; // No longer on header lines
|
||||
case EditTarget::ArrayIndex:
|
||||
case EditTarget::ArrayCount:
|
||||
@@ -1526,6 +1790,10 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
s = arrayElemCountSpanFor(*lm, lineText); break;
|
||||
case EditTarget::PointerTarget:
|
||||
s = pointerTargetSpanFor(*lm, lineText); break;
|
||||
case EditTarget::StaticExpr:
|
||||
if (lm->isStaticLine)
|
||||
s = staticExprSpanFor(*lm, lineText);
|
||||
break;
|
||||
case EditTarget::Source: break;
|
||||
}
|
||||
|
||||
@@ -1542,6 +1810,12 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
if (!s.valid && t == EditTarget::Name)
|
||||
s = headerNameSpan(*lm, lineText);
|
||||
|
||||
// Member lines: override Name/Value spans
|
||||
if (!s.valid && lm->isMemberLine) {
|
||||
if (t == EditTarget::Name) s = memberNameSpanFor(*lm, lineText);
|
||||
if (t == EditTarget::Value) s = memberValueSpanFor(*lm, lineText);
|
||||
}
|
||||
|
||||
out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/true);
|
||||
if (lineTextOut) *lineTextOut = lineText;
|
||||
return out.valid;
|
||||
@@ -1655,6 +1929,12 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
if (!ns.valid)
|
||||
ns = headerNameSpan(lm, lineText);
|
||||
|
||||
// Member lines: use name/value spans from line text (no type span)
|
||||
if (lm.isMemberLine) {
|
||||
ns = memberNameSpanFor(lm, lineText);
|
||||
vs = memberValueSpanFor(lm, lineText);
|
||||
}
|
||||
|
||||
if (inSpan(ts)) outTarget = EditTarget::Type;
|
||||
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
||||
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
||||
@@ -1693,6 +1973,10 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (obj == m_sci && event->type() == QEvent::KeyPress) {
|
||||
auto* ke = static_cast<QKeyEvent*>(event);
|
||||
if (ke->matches(QKeySequence::Find)) {
|
||||
showFindBar();
|
||||
return true;
|
||||
}
|
||||
bool handled = m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
|
||||
if (!handled && !m_editState.active) {
|
||||
// Clear hover on keyboard navigation (stale after scroll)
|
||||
@@ -1744,8 +2028,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
}
|
||||
|
||||
commitInlineEdit();
|
||||
m_currentSelIds.clear(); // stale — normal handler will re-establish
|
||||
// Fall through to normal click handler below
|
||||
m_currentSelIds.clear();
|
||||
return true; // consume — metadata was recomposed; stale coords unsafe
|
||||
}
|
||||
// Single-click on fold column (" - " / " + ") toggles fold
|
||||
// Other left-clicks emit nodeClicked for selection
|
||||
@@ -1786,15 +2070,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
// Single-click on editable token of already-selected node → edit
|
||||
int tLine, tCol; EditTarget t;
|
||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
||||
// Type/ArrayElementType/PointerTarget open a dismissible popup
|
||||
// (not inline text edit), so allow on first click without
|
||||
// requiring the node to be pre-selected.
|
||||
bool isPopupTarget = (t == EditTarget::Type
|
||||
|| t == EditTarget::ArrayElementType
|
||||
|| t == EditTarget::PointerTarget);
|
||||
if ((alreadySelected || isPopupTarget) && plain) {
|
||||
if (!alreadySelected)
|
||||
emit nodeClicked(h.line, h.nodeId, me->modifiers());
|
||||
if (alreadySelected && plain) {
|
||||
m_pendingClickNodeId = 0;
|
||||
return beginInlineEdit(t, tLine, tCol);
|
||||
}
|
||||
@@ -1976,6 +2252,12 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
return beginInlineEdit(EditTarget::Value);
|
||||
case Qt::Key_Insert:
|
||||
if (ke->modifiers() & Qt::ShiftModifier)
|
||||
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex32);
|
||||
else
|
||||
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex64);
|
||||
return true;
|
||||
case Qt::Key_Tab: {
|
||||
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
|
||||
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
||||
@@ -2151,7 +2433,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
||||
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
|
||||
return false;
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||
// Exception: static field names are always editable (they're function names, not hex labels)
|
||||
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine)
|
||||
return false;
|
||||
|
||||
QString lineText;
|
||||
@@ -2368,8 +2651,12 @@ void RcxEditor::commitInlineEdit() {
|
||||
if (m_editState.target == EditTarget::Type && editedText.isEmpty())
|
||||
editedText = m_editState.original;
|
||||
|
||||
// Grab resolved address from LineMeta before endInlineEdit clears state
|
||||
const LineMeta* lm = metaForLine(m_editState.line);
|
||||
uint64_t addr = lm ? lm->offsetAddr : 0;
|
||||
|
||||
auto info = endInlineEdit();
|
||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText);
|
||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText, addr);
|
||||
}
|
||||
|
||||
// ── Cancel inline edit ──
|
||||
@@ -2455,6 +2742,9 @@ void RcxEditor::showSourcePicker() {
|
||||
act->setChecked(m_savedSourceDisplay[i].active);
|
||||
act->setData(i);
|
||||
}
|
||||
menu.addSeparator();
|
||||
auto* clearAct = menu.addAction("Clear All");
|
||||
clearAct->setData(QStringLiteral("#clear"));
|
||||
}
|
||||
|
||||
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
||||
@@ -2466,11 +2756,15 @@ void RcxEditor::showSourcePicker() {
|
||||
|
||||
QAction* sel = menu.exec(pos);
|
||||
if (sel) {
|
||||
const LineMeta* lm = metaForLine(m_editState.line);
|
||||
uint64_t addr = lm ? lm->offsetAddr : 0;
|
||||
auto info = endInlineEdit();
|
||||
QString text = sel->text();
|
||||
if (sel->data().isValid())
|
||||
if (sel->data().toString() == QStringLiteral("#clear"))
|
||||
text = QStringLiteral("#clear");
|
||||
else if (sel->data().isValid())
|
||||
text = QStringLiteral("#saved:") + QString::number(sel->data().toInt());
|
||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
|
||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
|
||||
} else {
|
||||
cancelInlineEdit();
|
||||
}
|
||||
@@ -2602,11 +2896,18 @@ void RcxEditor::updateEditableIndicators(int line) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to check if a line's node is selected (handles footer IDs)
|
||||
// Helper to check if a line's node is selected (handles footer/array element IDs)
|
||||
auto isLineSelected = [this](const LineMeta* lm) -> bool {
|
||||
if (!lm) return false;
|
||||
bool isFooter = (lm->lineKind == LineKind::Footer);
|
||||
uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId;
|
||||
uint64_t checkId;
|
||||
if (lm->lineKind == LineKind::Footer)
|
||||
checkId = lm->nodeId | kFooterIdBit;
|
||||
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
|
||||
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
|
||||
else if (lm->isMemberLine && lm->subLine >= 0)
|
||||
checkId = makeMemberSelId(lm->nodeId, lm->subLine);
|
||||
else
|
||||
checkId = lm->nodeId;
|
||||
return m_currentSelIds.contains(checkId);
|
||||
};
|
||||
|
||||
@@ -3134,6 +3435,11 @@ void RcxEditor::setCommandRowText(const QString& line) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0);
|
||||
m_sci->setReadOnly(false);
|
||||
|
||||
// Suppress modification notifications during replace to avoid
|
||||
// QScintilla accessibility crash (textDeleted called with null text).
|
||||
long savedMask = m_sci->SendScintilla(QsciScintillaBase::SCI_GETMODEVENTMASK);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETMODEVENTMASK, 0);
|
||||
|
||||
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
|
||||
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0);
|
||||
QByteArray utf8 = s.toUtf8();
|
||||
@@ -3142,6 +3448,8 @@ void RcxEditor::setCommandRowText(const QString& line) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
|
||||
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETMODEVENTMASK, savedMask);
|
||||
|
||||
// Adjust saved cursor/anchor for length change in line 0
|
||||
long delta = (long)utf8.size() - oldLen;
|
||||
if (savedPos > end) savedPos += delta;
|
||||
|
||||
22
src/editor.h
22
src/editor.h
@@ -4,7 +4,9 @@
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QLineEdit;
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
|
||||
@@ -31,6 +33,8 @@ public:
|
||||
const LineMeta* metaForLine(int line) const;
|
||||
int currentNodeIndex() const;
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
void showFindBar();
|
||||
void dismissHistoryPopup();
|
||||
|
||||
// ── Column span computation ──
|
||||
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||
@@ -44,6 +48,7 @@ public:
|
||||
bool isEditing() const { return m_editState.active; }
|
||||
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
|
||||
void cancelInlineEdit();
|
||||
void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; }
|
||||
|
||||
void applySelectionOverlay(const QSet<uint64_t>& selIds);
|
||||
void setCommandRowText(const QString& line);
|
||||
@@ -60,6 +65,8 @@ public:
|
||||
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; }
|
||||
|
||||
@@ -69,10 +76,12 @@ signals:
|
||||
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);
|
||||
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
@@ -94,8 +103,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
|
||||
@@ -125,6 +138,7 @@ private:
|
||||
bool lastValidationOk = true; // track state to avoid redundant updates
|
||||
};
|
||||
InlineEditState m_editState;
|
||||
QStringList m_staticCompletions; // autocomplete words for StaticExpr editing
|
||||
|
||||
// ── Tab cycling state ──
|
||||
EditTarget m_lastTabTarget = EditTarget::Value;
|
||||
@@ -144,6 +158,12 @@ private:
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
// ── Find bar ──
|
||||
QWidget* m_findBarContainer = nullptr;
|
||||
QLineEdit* m_findBar = nullptr;
|
||||
long m_findPos = 0;
|
||||
void hideFindBar();
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
|
||||
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}
|
||||
]
|
||||
}
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
160984
src/examples/Vergilius_25H2.rcx
Normal file
160984
src/examples/Vergilius_25H2.rcx
Normal file
File diff suppressed because it is too large
Load Diff
11329
src/examples/t6zm.rcx
Normal file
11329
src/examples/t6zm.rcx
Normal file
File diff suppressed because it is too large
Load Diff
193
src/format.cpp
193
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
|
||||
@@ -71,38 +80,38 @@ static QString rawHex(uint64_t v, int digits) {
|
||||
return QString::number(v, 16).rightJustified(digits, '0');
|
||||
}
|
||||
|
||||
QString fmtInt8(int8_t v) { return hexVal((uint8_t)v); }
|
||||
QString fmtInt16(int16_t v) { return hexVal((uint16_t)v); }
|
||||
QString fmtInt32(int32_t v) { return hexVal((uint32_t)v); }
|
||||
QString fmtInt64(int64_t v) { return hexVal((uint64_t)v); }
|
||||
QString fmtInt8(int8_t v) { return QString::number(v); }
|
||||
QString fmtInt16(int16_t v) { return QString::number(v); }
|
||||
QString fmtInt32(int32_t v) { return QString::number(v); }
|
||||
QString fmtInt64(int64_t v) { return QString::number((qlonglong)v); }
|
||||
QString fmtUInt8(uint8_t v) { return hexVal(v); }
|
||||
QString fmtUInt16(uint16_t v) { return hexVal(v); }
|
||||
QString fmtUInt32(uint32_t v) { return hexVal(v); }
|
||||
QString fmtUInt64(uint64_t v) { return hexVal(v); }
|
||||
|
||||
QString fmtFloat(float v) {
|
||||
// Fixed 7-char body: digits + "." + decimals + "f"
|
||||
// Negative values get a '-' prefix (8 chars total), positive stay 7.
|
||||
if (std::isnan(v)) return QStringLiteral("NaN");
|
||||
if (std::isinf(v)) return v > 0 ? QStringLiteral("inff") : QStringLiteral("-inff");
|
||||
|
||||
// 6 significant digits — covers full single-precision range
|
||||
QString s = QString::number(v, 'g', 6);
|
||||
float av = std::fabs(v);
|
||||
if (av >= 100000.f)
|
||||
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
|
||||
|
||||
// If 'g' chose scientific notation, reformat as plain decimal
|
||||
if (s.contains('e') || s.contains('E')) {
|
||||
s = QString::number(v, 'f', 8);
|
||||
if (s.contains('.')) {
|
||||
int i = s.size() - 1;
|
||||
while (i > 0 && s[i] == '0') i--;
|
||||
if (s[i] == '.') i++; // keep at least one decimal digit
|
||||
s.truncate(i + 1);
|
||||
// body = digits + "." + decimals + "f", target exactly 7 chars.
|
||||
// Start with max decimals, reduce if integer part is wide or rounding overflows.
|
||||
for (int dec = 4; dec >= 0; dec--) {
|
||||
QString body = QString::number(av, 'f', dec);
|
||||
body += (dec == 0) ? QStringLiteral(".f") : QStringLiteral("f");
|
||||
if (body.size() == 7) {
|
||||
if (v < 0.f) body.prepend('-');
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.contains('.'))
|
||||
s += QStringLiteral(".f");
|
||||
else
|
||||
s += QLatin1Char('f');
|
||||
return s;
|
||||
// Rounding pushed past 99999 — use overflow cap
|
||||
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
|
||||
}
|
||||
QString fmtDouble(double v) {
|
||||
QString s = QString::number(v, 'g', 6);
|
||||
@@ -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);
|
||||
@@ -267,6 +281,30 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
}
|
||||
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);
|
||||
@@ -295,7 +333,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
int count = sizeForKind(node.kind) / 4;
|
||||
QStringList parts;
|
||||
for (int i = 0; i < count; i++)
|
||||
parts << fmtFloat(prov.readF32(addr + i * 4)).trimmed();
|
||||
parts << fmtFloat(prov.readF32(addr + i * 4));
|
||||
return parts.join(QStringLiteral(", "));
|
||||
}
|
||||
case NodeKind::Mat4x4: {
|
||||
@@ -304,7 +342,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
QString line = QStringLiteral("row%1 [").arg(subLine);
|
||||
for (int c = 0; c < 4; c++) {
|
||||
if (c > 0) line += QStringLiteral(", ");
|
||||
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();
|
||||
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4));
|
||||
}
|
||||
line += QStringLiteral("]");
|
||||
return line;
|
||||
@@ -341,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()
|
||||
@@ -369,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;
|
||||
}
|
||||
|
||||
@@ -640,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
|
||||
|
||||
@@ -68,6 +68,7 @@ struct GenContext {
|
||||
QString output;
|
||||
int padCounter = 0;
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr;
|
||||
bool emitAsserts = false;
|
||||
|
||||
QString uniquePadName() {
|
||||
return QStringLiteral("_pad%1").arg(padCounter++, 4, 16, QChar('0'));
|
||||
@@ -100,82 +101,93 @@ static void emitStruct(GenContext& ctx, uint64_t structId);
|
||||
|
||||
static const QChar kCommentMarker = QChar(0x01);
|
||||
|
||||
static QString offsetComment(int offset) {
|
||||
static QString offsetComment(int offset, bool isSizeof = false) {
|
||||
if (isSizeof)
|
||||
return QString(kCommentMarker) + QStringLiteral("// sizeof 0x%1").arg(QString::number(offset, 16).toUpper());
|
||||
return QString(kCommentMarker) + QStringLiteral("// 0x%1").arg(QString::number(offset, 16).toUpper());
|
||||
}
|
||||
|
||||
static QString emitField(GenContext& ctx, const Node& node) {
|
||||
static QString indent(int depth) {
|
||||
return QString(depth * 4, ' ');
|
||||
}
|
||||
|
||||
static QString emitField(GenContext& ctx, const Node& node, int depth, int baseOffset) {
|
||||
const NodeTree& tree = ctx.tree;
|
||||
QString ind = indent(depth);
|
||||
QString name = sanitizeIdent(node.name.isEmpty()
|
||||
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
|
||||
: node.name);
|
||||
QString oc = offsetComment(node.offset);
|
||||
QString oc = offsetComment(baseOffset + node.offset);
|
||||
|
||||
switch (node.kind) {
|
||||
case NodeKind::Vec2:
|
||||
return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
return ind + QStringLiteral("%1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
case NodeKind::Vec3:
|
||||
return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
return ind + QStringLiteral("%1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
case NodeKind::Vec4:
|
||||
return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
return ind + QStringLiteral("%1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
case NodeKind::Mat4x4:
|
||||
return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
return ind + QStringLiteral("%1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||
case NodeKind::UTF8:
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
||||
return ind + 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::Pointer32: {
|
||||
if (node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
QString target = ctx.structName(tree.nodes[refIdx]);
|
||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) +
|
||||
offsetComment(node.offset).replace(QStringLiteral("//"), QStringLiteral("// -> %1*").arg(target));
|
||||
}
|
||||
}
|
||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + oc;
|
||||
}
|
||||
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
||||
case NodeKind::Pointer32:
|
||||
case NodeKind::Pointer64: {
|
||||
if (node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
QString target = ctx.structName(tree.nodes[refIdx]);
|
||||
return QStringLiteral(" %1* %2;").arg(target, name) + oc;
|
||||
return ind + QStringLiteral("struct %1* %2;").arg(target, name) + oc;
|
||||
}
|
||||
}
|
||||
return QStringLiteral(" void* %1;").arg(name) + oc;
|
||||
// Native pointer: use void* when this is the target's natural pointer kind
|
||||
bool isNativePtr = (node.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4)
|
||||
|| (node.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8);
|
||||
if (isNativePtr)
|
||||
return ind + QStringLiteral("void* %1;").arg(name) + oc;
|
||||
// Cross-size pointer: fall back to raw integer type
|
||||
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(node.kind), name) + oc;
|
||||
}
|
||||
case NodeKind::FuncPtr32:
|
||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
||||
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
|
||||
case NodeKind::FuncPtr64:
|
||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
||||
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
|
||||
default:
|
||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
|
||||
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(node.kind), name) + oc;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Emit struct body (fields + padding) ──
|
||||
// ── Emit struct body (fields + padding) — Vergilius-style ──
|
||||
|
||||
static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
static void emitStructBody(GenContext& ctx, uint64_t structId,
|
||||
bool isUnion, int depth, int baseOffset) {
|
||||
const NodeTree& tree = ctx.tree;
|
||||
int idx = tree.indexOfId(structId);
|
||||
if (idx < 0) return;
|
||||
|
||||
int structSize = tree.structSpan(structId, &ctx.childMap);
|
||||
QString ind = indent(depth);
|
||||
|
||||
QVector<int> children = ctx.childMap.value(structId);
|
||||
QVector<int> allChildren = ctx.childMap.value(structId);
|
||||
QVector<int> children, staticIdxs;
|
||||
for (int ci : allChildren) {
|
||||
if (tree.nodes[ci].isStatic)
|
||||
staticIdxs.append(ci);
|
||||
else
|
||||
children.append(ci);
|
||||
}
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
// Helper: emit a padding/hex run as a single collapsed byte array
|
||||
auto emitPadRun = [&](int offset, int size) {
|
||||
auto emitPadRun = [&](int relOffset, int size) {
|
||||
if (size <= 0) return;
|
||||
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
|
||||
.arg(QStringLiteral("uint8_t"))
|
||||
ctx.output += ind + QStringLiteral("uint8_t %1[0x%2];%3\n")
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(size, 16).toUpper())
|
||||
.arg(offsetComment(offset));
|
||||
.arg(offsetComment(baseOffset + relOffset));
|
||||
};
|
||||
|
||||
int cursor = 0;
|
||||
@@ -189,13 +201,15 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
else
|
||||
childSize = child.byteSize();
|
||||
|
||||
// Gap before this field
|
||||
if (child.offset > cursor)
|
||||
emitPadRun(cursor, child.offset - cursor);
|
||||
else if (child.offset < cursor)
|
||||
ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
|
||||
.arg(QString::number(child.offset, 16).toUpper())
|
||||
.arg(QString::number(cursor, 16).toUpper());
|
||||
// Gap/overlap handling (skip for unions)
|
||||
if (!isUnion) {
|
||||
if (child.offset > cursor)
|
||||
emitPadRun(cursor, child.offset - cursor);
|
||||
else if (child.offset < cursor)
|
||||
ctx.output += ind + QStringLiteral("// WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
|
||||
.arg(QString::number(baseOffset + child.offset, 16).toUpper())
|
||||
.arg(QString::number(baseOffset + cursor, 16).toUpper());
|
||||
}
|
||||
|
||||
// Collapse consecutive hex nodes into a single padding array
|
||||
if (isHexNode(child.kind)) {
|
||||
@@ -206,8 +220,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
const Node& next = tree.nodes[children[j]];
|
||||
if (!isHexNode(next.kind)) break;
|
||||
int nextSize = next.byteSize();
|
||||
// Allow gaps within the run (they become part of the pad)
|
||||
if (next.offset < runEnd) break; // overlap — stop merging
|
||||
if (next.offset < runEnd) break;
|
||||
runEnd = next.offset + nextSize;
|
||||
j++;
|
||||
}
|
||||
@@ -219,10 +232,53 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
|
||||
// Emit the field
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
emitStruct(ctx, child.id);
|
||||
QString typeName = ctx.structName(child);
|
||||
QString fieldName = sanitizeIdent(child.name);
|
||||
ctx.output += QStringLiteral(" %1 %2;%3\n").arg(typeName, fieldName, offsetComment(child.offset));
|
||||
// Bitfield container — emit inline bitfield members
|
||||
if (child.classKeyword == QStringLiteral("bitfield")
|
||||
&& !child.bitfieldMembers.isEmpty()) {
|
||||
QString bfType = ctx.cType(child.elementKind);
|
||||
if (bfType.isEmpty()) bfType = QStringLiteral("uint32_t");
|
||||
QString fieldName = child.name.isEmpty()
|
||||
? QString() : QStringLiteral(" ") + sanitizeIdent(child.name);
|
||||
ctx.output += ind + QStringLiteral("struct\n");
|
||||
ctx.output += ind + QStringLiteral("{\n");
|
||||
QString bfInd = indent(depth + 1);
|
||||
for (const auto& m : child.bitfieldMembers) {
|
||||
ctx.output += bfInd + bfType + QStringLiteral(" ")
|
||||
+ sanitizeIdent(m.name) + QStringLiteral(" : ")
|
||||
+ QString::number(m.bitWidth) + QStringLiteral(";")
|
||||
+ offsetComment(baseOffset + child.offset)
|
||||
+ QStringLiteral("\n");
|
||||
}
|
||||
ctx.output += ind + QStringLiteral("}") + fieldName + QStringLiteral(";")
|
||||
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
|
||||
} else {
|
||||
|
||||
bool isAnonymous = child.structTypeName.isEmpty();
|
||||
|
||||
if (isAnonymous) {
|
||||
// Inline anonymous struct/union
|
||||
QString kw = child.resolvedClassKeyword();
|
||||
ctx.output += ind + kw + QStringLiteral("\n");
|
||||
ctx.output += ind + QStringLiteral("{\n");
|
||||
bool childIsUnion = (kw == QStringLiteral("union"));
|
||||
emitStructBody(ctx, child.id, childIsUnion, depth + 1,
|
||||
baseOffset + child.offset);
|
||||
QString fieldName = child.name.isEmpty()
|
||||
? QString() : QStringLiteral(" ") + sanitizeIdent(child.name);
|
||||
ctx.output += ind + QStringLiteral("}") + fieldName + QStringLiteral(";")
|
||||
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
|
||||
} else {
|
||||
// Named struct — reference by name with struct keyword prefix
|
||||
QString kw = child.resolvedClassKeyword();
|
||||
if (kw == QStringLiteral("enum") && child.enumMembers.isEmpty())
|
||||
kw = QStringLiteral("struct");
|
||||
QString typeName = sanitizeIdent(child.structTypeName);
|
||||
QString fieldName = sanitizeIdent(child.name);
|
||||
ctx.output += ind + kw + QStringLiteral(" ") + typeName
|
||||
+ QStringLiteral(" ") + fieldName + QStringLiteral(";")
|
||||
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
|
||||
}
|
||||
} // end bitfield else
|
||||
} else if (child.kind == NodeKind::Array) {
|
||||
QVector<int> arrayKids = ctx.childMap.value(child.id);
|
||||
bool hasStructChild = false;
|
||||
@@ -231,7 +287,6 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
for (int ak : arrayKids) {
|
||||
if (tree.nodes[ak].kind == NodeKind::Struct) {
|
||||
hasStructChild = true;
|
||||
emitStruct(ctx, tree.nodes[ak].id);
|
||||
elemTypeName = ctx.structName(tree.nodes[ak]);
|
||||
break;
|
||||
}
|
||||
@@ -239,14 +294,16 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
|
||||
QString fieldName = sanitizeIdent(child.name);
|
||||
if (hasStructChild && !elemTypeName.isEmpty()) {
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
|
||||
.arg(elemTypeName, fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
|
||||
ctx.output += ind + QStringLiteral("struct %1 %2[%3];%4\n")
|
||||
.arg(elemTypeName, fieldName).arg(child.arrayLen)
|
||||
.arg(offsetComment(baseOffset + child.offset));
|
||||
} else {
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
|
||||
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
|
||||
ctx.output += ind + QStringLiteral("%1 %2[%3];%4\n")
|
||||
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen)
|
||||
.arg(offsetComment(baseOffset + child.offset));
|
||||
}
|
||||
} else {
|
||||
ctx.output += emitField(ctx, child) + QStringLiteral("\n");
|
||||
ctx.output += emitField(ctx, child, depth, baseOffset) + QStringLiteral("\n");
|
||||
}
|
||||
|
||||
int childEnd = child.offset + childSize;
|
||||
@@ -254,12 +311,20 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
i++;
|
||||
}
|
||||
|
||||
// Tail padding
|
||||
if (cursor < structSize)
|
||||
// Tail padding (skip for unions)
|
||||
if (!isUnion && cursor < structSize)
|
||||
emitPadRun(cursor, structSize - cursor);
|
||||
|
||||
// Emit static field comments (static fields are runtime-only, not part of struct layout)
|
||||
for (int si : staticIdxs) {
|
||||
const Node& sf = tree.nodes[si];
|
||||
QString sfType = sf.structTypeName.isEmpty() ? ctx.cType(sf.kind) : sf.structTypeName;
|
||||
ctx.output += ind + QStringLiteral("// static: %1 %2 @ %3\n")
|
||||
.arg(sfType, sanitizeIdent(sf.name), sf.offsetExpr);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Emit a complete struct definition ──
|
||||
// ── Emit a complete top-level struct definition (Vergilius-style) ──
|
||||
|
||||
static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
if (ctx.emittedIds.contains(structId)) return;
|
||||
@@ -275,19 +340,12 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For arrays, we don't emit a top-level struct — the array itself
|
||||
// is a field inside its parent. But we do emit struct element types.
|
||||
if (node.kind == NodeKind::Array) {
|
||||
QVector<int> kids = ctx.childMap.value(structId);
|
||||
for (int ki : kids) {
|
||||
if (ctx.tree.nodes[ki].kind == NodeKind::Struct)
|
||||
emitStruct(ctx, ctx.tree.nodes[ki].id);
|
||||
}
|
||||
ctx.visiting.remove(structId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate by struct type name (different nodes may share the same type)
|
||||
// Deduplicate by struct type name
|
||||
QString typeName = ctx.structName(node);
|
||||
if (ctx.emittedTypeNames.contains(typeName)) {
|
||||
ctx.emittedIds.insert(structId);
|
||||
@@ -295,47 +353,39 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit nested struct types first (dependency order)
|
||||
QVector<int> children = ctx.childMap.value(structId);
|
||||
for (int ci : children) {
|
||||
const Node& child = ctx.tree.nodes[ci];
|
||||
if (child.kind == NodeKind::Struct)
|
||||
emitStruct(ctx, child.id);
|
||||
else if (child.kind == NodeKind::Array) {
|
||||
QVector<int> arrayKids = ctx.childMap.value(child.id);
|
||||
for (int ak : arrayKids) {
|
||||
if (ctx.tree.nodes[ak].kind == NodeKind::Struct)
|
||||
emitStruct(ctx, ctx.tree.nodes[ak].id);
|
||||
}
|
||||
}
|
||||
// Forward-declare pointer target types if they're outside this subtree
|
||||
if (child.kind == NodeKind::Pointer64 && child.refId != 0) {
|
||||
int refIdx = ctx.tree.indexOfId(child.refId);
|
||||
if (refIdx >= 0 && !ctx.emittedIds.contains(child.refId)
|
||||
&& !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");
|
||||
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
||||
ctx.forwardDeclared.insert(child.refId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.emittedIds.insert(structId);
|
||||
ctx.emittedTypeNames.insert(typeName);
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
QString kw = node.resolvedClassKeyword();
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
|
||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
||||
|
||||
emitStructBody(ctx, structId);
|
||||
// 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;
|
||||
}
|
||||
|
||||
ctx.output += QStringLiteral("};\n");
|
||||
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n")
|
||||
.arg(typeName)
|
||||
.arg(QString::number(structSize, 16).toUpper());
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct");
|
||||
|
||||
ctx.output += kw + QStringLiteral(" ") + typeName + QStringLiteral("\n{\n");
|
||||
|
||||
emitStructBody(ctx, structId, kw == QStringLiteral("union"), 1, 0);
|
||||
|
||||
ctx.output += QStringLiteral("};")
|
||||
+ offsetComment(structSize, true)
|
||||
+ QStringLiteral("\n");
|
||||
if (ctx.emitAsserts)
|
||||
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n")
|
||||
.arg(typeName)
|
||||
.arg(QString::number(structSize, 16).toUpper());
|
||||
ctx.output += QStringLiteral("\n");
|
||||
|
||||
ctx.visiting.remove(structId);
|
||||
}
|
||||
@@ -389,14 +439,15 @@ static QString alignComments(const QString& raw) {
|
||||
// ── Public API ──
|
||||
|
||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases) {
|
||||
const QHash<NodeKind, QString>* typeAliases,
|
||||
bool emitAsserts) {
|
||||
int idx = tree.indexOfId(rootStructId);
|
||||
if (idx < 0) return {};
|
||||
|
||||
const Node& root = tree.nodes[idx];
|
||||
if (root.kind != NodeKind::Struct) return {};
|
||||
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
|
||||
|
||||
ctx.output += QStringLiteral("#pragma once\n\n");
|
||||
|
||||
@@ -406,8 +457,9 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
}
|
||||
|
||||
QString renderCppAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases) {
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
|
||||
const QHash<NodeKind, QString>* typeAliases,
|
||||
bool emitAsserts) {
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
|
||||
|
||||
ctx.output += QStringLiteral("#pragma once\n\n");
|
||||
|
||||
|
||||
@@ -9,11 +9,13 @@ namespace rcx {
|
||||
// Generate C++ struct definitions for a single root struct and all
|
||||
// nested/referenced types reachable from it.
|
||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr);
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// Generate C++ struct definitions for every root-level struct (full SDK).
|
||||
QString renderCppAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr);
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// Null generator placeholder (returns empty string).
|
||||
QString renderNull(const NodeTree& tree, uint64_t rootStructId);
|
||||
|
||||
BIN
src/icons/class.icns
Normal file
BIN
src/icons/class.icns
Normal file
Binary file not shown.
@@ -115,6 +115,24 @@ bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* er
|
||||
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;
|
||||
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
|
||||
@@ -80,15 +80,19 @@ static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
|
||||
{ 30, NodeKind::Array }, // ClassPointerArray
|
||||
};
|
||||
|
||||
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
|
||||
static NodeKind lookupKind(int xmlType, XmlVersion ver, int ptrSize = 8) {
|
||||
NodeKind k = NodeKind::Hex8;
|
||||
if (ver == XmlVersion::V2016) {
|
||||
for (const auto& e : kTypeMap2016)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
if (e.xmlType == xmlType) { k = e.kind; break; }
|
||||
} else {
|
||||
for (const auto& e : kTypeMap2013)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
if (e.xmlType == xmlType) { k = e.kind; break; }
|
||||
}
|
||||
return NodeKind::Hex8; // fallback
|
||||
// Remap pointer types for 32-bit targets
|
||||
if (ptrSize < 8 && k == NodeKind::Pointer64)
|
||||
k = NodeKind::Pointer32;
|
||||
return k;
|
||||
}
|
||||
|
||||
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
|
||||
@@ -135,7 +139,7 @@ struct PendingRef {
|
||||
QString className;
|
||||
};
|
||||
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg, int pointerSize) {
|
||||
qDebug() << "[ImportXML] Opening file:" << filePath;
|
||||
|
||||
QFile file(filePath);
|
||||
@@ -152,6 +156,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
tree.pointerSize = pointerSize;
|
||||
|
||||
// Class name → struct node ID (for pointer resolution)
|
||||
QHash<QString, uint64_t> classIds;
|
||||
@@ -249,7 +254,7 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeKind kind = lookupKind(xmlType, version);
|
||||
NodeKind kind = lookupKind(xmlType, version, pointerSize);
|
||||
|
||||
// Handle ClassInstanceArray: read child <Array> element
|
||||
if (isClassInstanceArrayType(xmlType, version)) {
|
||||
@@ -371,7 +376,6 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
auto it = classIds.find(ref.className);
|
||||
if (it != classIds.end()) {
|
||||
tree.nodes[nodeIdx].refId = it.value();
|
||||
tree.invalidateIdCache();
|
||||
resolved++;
|
||||
} else {
|
||||
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
||||
@@ -5,7 +5,9 @@ namespace rcx {
|
||||
|
||||
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
|
||||
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
|
||||
// pointerSize: 4 for 32-bit targets, 8 for 64-bit (default).
|
||||
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr,
|
||||
int pointerSize = 8);
|
||||
|
||||
} // namespace rcx
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "import_source.h"
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
#include <QRegularExpression>
|
||||
#include <QDebug>
|
||||
@@ -13,8 +14,12 @@ struct TypeInfo {
|
||||
int size; // bytes (0 = dynamic/pointer)
|
||||
};
|
||||
|
||||
static QHash<QString, TypeInfo> buildTypeTable() {
|
||||
static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
|
||||
QHash<QString, TypeInfo> t;
|
||||
// Pointer/size_t kinds depend on target architecture
|
||||
NodeKind ptrKind = (ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
NodeKind uintpKind = (ptrSize >= 8) ? NodeKind::UInt64 : NodeKind::UInt32;
|
||||
NodeKind intpKind = (ptrSize >= 8) ? NodeKind::Int64 : NodeKind::Int32;
|
||||
|
||||
// stdint.h
|
||||
t[QStringLiteral("uint8_t")] = {NodeKind::UInt8, 1};
|
||||
@@ -84,35 +89,35 @@ static QHash<QString, TypeInfo> buildTypeTable() {
|
||||
t[QStringLiteral("LONG64")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("INT64")] = {NodeKind::Int64, 8};
|
||||
|
||||
// Platform pointer-size types
|
||||
t[QStringLiteral("PVOID")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPVOID")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HANDLE")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HMODULE")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HWND")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("HINSTANCE")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("SIZE_T")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("ULONG_PTR")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("UINT_PTR")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("DWORD_PTR")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("LONG_PTR")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("INT_PTR")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("SSIZE_T")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("uintptr_t")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("intptr_t")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("size_t")] = {NodeKind::UInt64, 8};
|
||||
t[QStringLiteral("ptrdiff_t")] = {NodeKind::Int64, 8};
|
||||
t[QStringLiteral("ssize_t")] = {NodeKind::Int64, 8};
|
||||
// Platform pointer-size types (depend on target architecture)
|
||||
t[QStringLiteral("PVOID")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPVOID")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HANDLE")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HMODULE")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HWND")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("HINSTANCE")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("SIZE_T")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("ULONG_PTR")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("UINT_PTR")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("DWORD_PTR")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("LONG_PTR")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("INT_PTR")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("SSIZE_T")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("uintptr_t")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("intptr_t")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("size_t")] = {uintpKind, ptrSize};
|
||||
t[QStringLiteral("ptrdiff_t")] = {intpKind, ptrSize};
|
||||
t[QStringLiteral("ssize_t")] = {intpKind, ptrSize};
|
||||
|
||||
// Pointer type aliases
|
||||
t[QStringLiteral("PCHAR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPCSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PCSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PWSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPWSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("LPCWSTR")]= {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PCWSTR")] = {NodeKind::Pointer64, 8};
|
||||
t[QStringLiteral("PCHAR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPCSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("PCSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("PWSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPWSTR")] = {ptrKind, ptrSize};
|
||||
t[QStringLiteral("LPCWSTR")]= {ptrKind, ptrSize};
|
||||
t[QStringLiteral("PCWSTR")] = {ptrKind, ptrSize};
|
||||
|
||||
return t;
|
||||
}
|
||||
@@ -285,13 +290,16 @@ struct ParsedField {
|
||||
int commentOffset = -1; // from // 0xNN (-1 = none)
|
||||
int bitfieldWidth = -1; // -1 = not a bitfield
|
||||
QString pointerTarget; // for Type* -> the type name
|
||||
bool isUnion = false; // union container
|
||||
QVector<ParsedField> unionMembers; // children of union
|
||||
};
|
||||
|
||||
struct ParsedStruct {
|
||||
QString name;
|
||||
QString keyword; // "struct" or "class"
|
||||
QString keyword; // "struct", "class", or "enum"
|
||||
QVector<ParsedField> fields;
|
||||
int declaredSize = -1; // from static_assert
|
||||
QVector<QPair<QString, int64_t>> enumValues; // for keyword="enum"
|
||||
};
|
||||
|
||||
struct PendingRef {
|
||||
@@ -378,8 +386,7 @@ struct Parser {
|
||||
} else if (checkIdent("typedef")) {
|
||||
parseTypedef();
|
||||
} else if (checkIdent("enum")) {
|
||||
skipToSemiOrBrace();
|
||||
if (check(TokKind::RBrace)) { advance(); match(TokKind::Semi); }
|
||||
parseEnumDef();
|
||||
} else if (peek().kind == TokKind::Hash) {
|
||||
// preprocessor (shouldn't reach here if tokenizer skipped them)
|
||||
advance();
|
||||
@@ -464,12 +471,18 @@ struct Parser {
|
||||
// Might be "struct TypeName fieldName;" - fall through to field parsing
|
||||
}
|
||||
|
||||
// Union: pick first member only
|
||||
// Union: create container with all members
|
||||
if (checkIdent("union")) {
|
||||
parseUnion(ps);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Enum definition inside struct
|
||||
if (checkIdent("enum")) {
|
||||
parseEnumDef();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Static assert inside struct
|
||||
if (checkIdent("static_assert")) {
|
||||
parseStaticAssert();
|
||||
@@ -489,33 +502,76 @@ struct Parser {
|
||||
void parseUnion(ParsedStruct& ps) {
|
||||
advance(); // skip "union"
|
||||
|
||||
// Optional union name
|
||||
// Optional union tag name (before {)
|
||||
if (check(TokKind::Ident) && peek(1).kind == TokKind::LBrace) {
|
||||
advance(); // skip union name
|
||||
advance(); // skip union tag name
|
||||
}
|
||||
|
||||
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||
|
||||
// Parse first member of union
|
||||
bool gotFirst = false;
|
||||
// Parse ALL members of the union
|
||||
ParsedField unionField;
|
||||
unionField.isUnion = true;
|
||||
|
||||
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||
if (!gotFirst) {
|
||||
ParsedField field;
|
||||
if (parseField(field)) {
|
||||
ps.fields.append(field);
|
||||
gotFirst = true;
|
||||
} else {
|
||||
advance();
|
||||
// Handle nested unions inside this union
|
||||
if (checkIdent("union")) {
|
||||
// Recurse: create a sub-union ParsedStruct temporarily,
|
||||
// then steal its fields as a nested union member
|
||||
ParsedStruct tmp;
|
||||
parseUnion(tmp);
|
||||
for (auto& f : tmp.fields)
|
||||
unionField.unionMembers.append(f);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle anonymous struct inside union: struct { ... };
|
||||
if ((checkIdent("struct") || checkIdent("class")) && peek(1).kind == TokKind::LBrace) {
|
||||
advance(); // skip "struct"
|
||||
advance(); // skip "{"
|
||||
int depth = 1;
|
||||
while (peek().kind != TokKind::Eof && depth > 0) {
|
||||
if (peek().kind == TokKind::LBrace) depth++;
|
||||
else if (peek().kind == TokKind::RBrace) depth--;
|
||||
if (depth > 0) advance();
|
||||
}
|
||||
if (check(TokKind::RBrace)) advance();
|
||||
if (check(TokKind::Ident)) advance(); // optional field name
|
||||
match(TokKind::Semi);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle nested named struct definition inside union
|
||||
if ((checkIdent("struct") || checkIdent("class")) &&
|
||||
peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
|
||||
parseStructOrForward();
|
||||
continue;
|
||||
}
|
||||
|
||||
ParsedField field;
|
||||
if (parseField(field)) {
|
||||
unionField.unionMembers.append(field);
|
||||
} else {
|
||||
// Skip remaining union members
|
||||
skipToSemiOrBrace();
|
||||
advance();
|
||||
}
|
||||
}
|
||||
match(TokKind::RBrace);
|
||||
// Optional field name after union close
|
||||
if (check(TokKind::Ident)) advance();
|
||||
|
||||
// Optional field name after union close: union { ... } u3;
|
||||
if (check(TokKind::Ident)) {
|
||||
unionField.name = advance().text;
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
|
||||
// Determine offset from first member with a known offset
|
||||
for (const auto& m : unionField.unionMembers) {
|
||||
if (m.commentOffset >= 0) {
|
||||
unionField.commentOffset = m.commentOffset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ps.fields.append(unionField);
|
||||
}
|
||||
|
||||
bool parseField(ParsedField& field) {
|
||||
@@ -719,6 +775,90 @@ struct Parser {
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
}
|
||||
|
||||
void parseEnumDef() {
|
||||
advance(); // skip "enum"
|
||||
|
||||
// Optional "class" or "struct" (enum class)
|
||||
if (checkIdent("class") || checkIdent("struct"))
|
||||
advance();
|
||||
|
||||
// Optional name
|
||||
QString name;
|
||||
if (check(TokKind::Ident) && peek(1).kind != TokKind::Semi) {
|
||||
// Could be: enum Name { ... }; or enum Name : Type { ... };
|
||||
// But NOT: enum Name; (forward decl) or enum Name field; (field usage)
|
||||
if (peek(1).kind == TokKind::LBrace || peek(1).kind == TokKind::Colon) {
|
||||
name = advance().text;
|
||||
} else {
|
||||
// Not an enum definition — revert. This might be a field like "enum Foo bar;"
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional underlying type: enum Name : uint8_t { ... }
|
||||
if (check(TokKind::Colon)) {
|
||||
advance();
|
||||
parseTypeName(); // skip underlying type
|
||||
}
|
||||
|
||||
// Forward declaration: enum Name;
|
||||
if (check(TokKind::Semi)) {
|
||||
advance();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||
|
||||
ParsedStruct ps;
|
||||
ps.name = name;
|
||||
ps.keyword = QStringLiteral("enum");
|
||||
|
||||
// Parse enum members: Name [= Value], ...
|
||||
int64_t nextValue = 0;
|
||||
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||
if (!check(TokKind::Ident)) { advance(); continue; }
|
||||
QString memberName = advance().text;
|
||||
int64_t memberValue = nextValue;
|
||||
|
||||
if (check(TokKind::Equals)) {
|
||||
advance();
|
||||
// Parse value: could be number, negative number, or expression
|
||||
bool negative = false;
|
||||
if (peek().kind == TokKind::Other && peek().text == QStringLiteral("-")) {
|
||||
negative = true;
|
||||
advance();
|
||||
}
|
||||
if (check(TokKind::Number)) {
|
||||
bool ok;
|
||||
QString numText = peek().text;
|
||||
if (numText.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||
memberValue = numText.mid(2).toLongLong(&ok, 16);
|
||||
else
|
||||
memberValue = numText.toLongLong(&ok);
|
||||
if (negative) memberValue = -memberValue;
|
||||
advance();
|
||||
} else {
|
||||
// Complex expression — skip to comma or brace
|
||||
while (peek().kind != TokKind::Comma &&
|
||||
peek().kind != TokKind::RBrace &&
|
||||
peek().kind != TokKind::Eof)
|
||||
advance();
|
||||
}
|
||||
}
|
||||
|
||||
ps.enumValues.append({memberName, memberValue});
|
||||
nextValue = memberValue + 1;
|
||||
|
||||
// Skip comma between members
|
||||
match(TokKind::Comma);
|
||||
}
|
||||
match(TokKind::RBrace);
|
||||
match(TokKind::Semi);
|
||||
|
||||
if (!ps.name.isEmpty())
|
||||
structs.append(ps);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Padding field detection ──
|
||||
@@ -758,9 +898,331 @@ static void emitHexPadding(NodeTree& tree, uint64_t parentId, int offset, int si
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bitfield grouping: emit a bitfield container with named members ──
|
||||
|
||||
static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset,
|
||||
const QVector<ParsedField>& fields,
|
||||
int startIdx, int endIdx) {
|
||||
int totalBits = 0;
|
||||
for (int i = startIdx; i < endIdx; i++)
|
||||
totalBits += fields[i].bitfieldWidth;
|
||||
int bytes = (totalBits + 7) / 8;
|
||||
NodeKind containerKind;
|
||||
if (bytes <= 1) containerKind = NodeKind::Hex8;
|
||||
else if (bytes <= 2) containerKind = NodeKind::Hex16;
|
||||
else if (bytes <= 4) containerKind = NodeKind::Hex32;
|
||||
else containerKind = NodeKind::Hex64;
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = containerKind;
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
n.collapsed = false;
|
||||
|
||||
// Populate bitfield members with computed bit offsets
|
||||
uint8_t bitOffset = 0;
|
||||
for (int i = startIdx; i < endIdx; i++) {
|
||||
BitfieldMember bm;
|
||||
bm.name = fields[i].name;
|
||||
bm.bitOffset = bitOffset;
|
||||
bm.bitWidth = (uint8_t)fields[i].bitfieldWidth;
|
||||
n.bitfieldMembers.append(bm);
|
||||
bitOffset += bm.bitWidth;
|
||||
}
|
||||
|
||||
tree.addNode(n);
|
||||
}
|
||||
|
||||
// ── NodeTree builder: recursive field emitter ──
|
||||
|
||||
struct BuildContext {
|
||||
NodeTree& tree;
|
||||
const QHash<QString, TypeInfo>& typeTable;
|
||||
QHash<QString, uint64_t>& classIds;
|
||||
QVector<PendingRef>& pendingRefs;
|
||||
bool useCommentOffsets;
|
||||
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
|
||||
int ptrSize = 8; // target pointer size (4 or 8)
|
||||
};
|
||||
|
||||
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
const QVector<ParsedField>& fields) {
|
||||
int computedOffset = 0;
|
||||
|
||||
for (int fi = 0; fi < fields.size(); fi++) {
|
||||
const auto& field = fields[fi];
|
||||
|
||||
// Bitfield group: consume consecutive bitfields, emit bitfield container
|
||||
if (field.bitfieldWidth >= 0) {
|
||||
int groupOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
groupOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
groupOffset = computedOffset;
|
||||
int startIdx = fi;
|
||||
int totalBits = 0;
|
||||
while (fi < fields.size() && fields[fi].bitfieldWidth >= 0) {
|
||||
totalBits += fields[fi].bitfieldWidth;
|
||||
fi++;
|
||||
}
|
||||
fi--; // compensate for outer loop increment
|
||||
if (totalBits > 0)
|
||||
emitBitfieldGroup(ctx.tree, parentId, groupOffset,
|
||||
fields, startIdx, fi + 1);
|
||||
int bytes = (totalBits + 7) / 8;
|
||||
int nodeSize = (bytes <= 1) ? 1 : (bytes <= 2) ? 2 : (bytes <= 4) ? 4 : 8;
|
||||
computedOffset = groupOffset + nodeSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Union container field
|
||||
if (field.isUnion) {
|
||||
int unionOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
unionOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
unionOffset = computedOffset;
|
||||
|
||||
Node unionNode;
|
||||
unionNode.kind = NodeKind::Struct;
|
||||
unionNode.classKeyword = QStringLiteral("union");
|
||||
unionNode.name = field.name;
|
||||
unionNode.parentId = parentId;
|
||||
unionNode.offset = unionOffset;
|
||||
unionNode.collapsed = true;
|
||||
|
||||
int unionIdx = ctx.tree.addNode(unionNode);
|
||||
uint64_t unionId = ctx.tree.nodes[unionIdx].id;
|
||||
|
||||
// Build each union member independently so each starts at offset 0
|
||||
int absUnionOffset = baseOffset + unionOffset;
|
||||
for (const auto& member : field.unionMembers) {
|
||||
QVector<ParsedField> single;
|
||||
single.append(member);
|
||||
buildFields(ctx, unionId, absUnionOffset, single);
|
||||
}
|
||||
|
||||
// Advance computed offset past the union (max member size)
|
||||
int unionSpan = ctx.tree.structSpan(unionId);
|
||||
computedOffset = unionOffset + (unionSpan > 0 ? unionSpan : 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
int fieldOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
fieldOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
fieldOffset = computedOffset;
|
||||
|
||||
// Resolve type
|
||||
auto typeIt = ctx.typeTable.find(field.typeName);
|
||||
bool knownType = typeIt != ctx.typeTable.end();
|
||||
|
||||
// Pointer field
|
||||
if (field.isPointer) {
|
||||
Node n;
|
||||
n.kind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
|
||||
if (!field.pointerTarget.isEmpty() &&
|
||||
field.pointerTarget != QStringLiteral("void")) {
|
||||
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
||||
}
|
||||
|
||||
computedOffset = fieldOffset + ctx.ptrSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Enum-typed field: emit as UInt32 with refId to enum definition
|
||||
if (!knownType && ctx.enumNames.contains(field.typeName)) {
|
||||
int elemSize = 4;
|
||||
NodeKind elemKind = NodeKind::UInt32;
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = elemKind;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * elemSize;
|
||||
} else {
|
||||
Node n;
|
||||
n.kind = elemKind;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
computedOffset = fieldOffset + elemSize;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine base type info
|
||||
NodeKind baseKind = NodeKind::Hex8;
|
||||
int baseSize = 1;
|
||||
bool isStructType = false;
|
||||
|
||||
if (knownType) {
|
||||
baseKind = typeIt->kind;
|
||||
baseSize = typeIt->size;
|
||||
} else {
|
||||
isStructType = true;
|
||||
}
|
||||
|
||||
// Padding fields
|
||||
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
|
||||
int totalSize = baseSize;
|
||||
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
|
||||
emitHexPadding(ctx.tree, parentId, fieldOffset, totalSize);
|
||||
computedOffset = fieldOffset + totalSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Array fields
|
||||
if (!field.arraySizes.isEmpty() && !isStructType) {
|
||||
int firstDim = field.arraySizes.value(0, 1);
|
||||
if (firstDim <= 0) firstDim = 1;
|
||||
|
||||
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
||||
field.typeName == QStringLiteral("char")) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF8;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF16;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim * 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
|
||||
if (firstDim == 2) {
|
||||
Node n; n.kind = NodeKind::Vec2; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 8; continue;
|
||||
}
|
||||
if (firstDim == 3) {
|
||||
Node n; n.kind = NodeKind::Vec3; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 12; continue;
|
||||
}
|
||||
if (firstDim == 4) {
|
||||
Node n; n.kind = NodeKind::Vec4; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 16; continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
|
||||
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
|
||||
Node n; n.kind = NodeKind::Mat4x4; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
|
||||
}
|
||||
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = baseKind;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * baseSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Struct-type field
|
||||
if (isStructType) {
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = NodeKind::Struct;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
continue;
|
||||
}
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simple primitive field
|
||||
Node n;
|
||||
n.kind = baseKind;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + baseSize;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check if any field (or union member) has a comment offset ──
|
||||
|
||||
static bool hasAnyCommentOffset(const QVector<ParsedField>& fields) {
|
||||
for (const auto& f : fields) {
|
||||
if (f.commentOffset >= 0) return true;
|
||||
if (f.isUnion && hasAnyCommentOffset(f.unionMembers)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── NodeTree builder ──
|
||||
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int pointerSize) {
|
||||
if (sourceCode.trimmed().isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Empty source code");
|
||||
return {};
|
||||
@@ -775,12 +1237,12 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
parser.parse();
|
||||
|
||||
if (parser.structs.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No struct definitions found");
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No struct or enum definitions found");
|
||||
return {};
|
||||
}
|
||||
|
||||
// Build type table
|
||||
QHash<QString, TypeInfo> typeTable = buildTypeTable();
|
||||
// Build type table (pointer-size types depend on target architecture)
|
||||
QHash<QString, TypeInfo> typeTable = buildTypeTable(pointerSize);
|
||||
|
||||
// Register typedefs into type table
|
||||
for (auto it = parser.typedefs.begin(); it != parser.typedefs.end(); ++it) {
|
||||
@@ -791,6 +1253,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
tree.pointerSize = pointerSize;
|
||||
|
||||
QHash<QString, uint64_t> classIds;
|
||||
QVector<PendingRef> pendingRefs;
|
||||
@@ -798,13 +1261,19 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
// Determine offset mode: if ANY field in ANY struct has a comment offset, use comment mode
|
||||
bool useCommentOffsets = false;
|
||||
for (const auto& ps : parser.structs) {
|
||||
for (const auto& f : ps.fields) {
|
||||
if (f.commentOffset >= 0) { useCommentOffsets = true; break; }
|
||||
}
|
||||
if (useCommentOffsets) break;
|
||||
if (hasAnyCommentOffset(ps.fields)) { useCommentOffsets = true; break; }
|
||||
}
|
||||
|
||||
// Build nodes for each struct
|
||||
// Collect enum type names for field-type detection
|
||||
QSet<QString> enumNames;
|
||||
for (const auto& ps : parser.structs) {
|
||||
if (ps.keyword == QStringLiteral("enum") && !ps.name.isEmpty())
|
||||
enumNames.insert(ps.name);
|
||||
}
|
||||
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames, pointerSize};
|
||||
|
||||
// Build nodes for each struct/enum
|
||||
for (const auto& ps : parser.structs) {
|
||||
Node structNode;
|
||||
structNode.kind = NodeKind::Struct;
|
||||
@@ -815,222 +1284,21 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
structNode.offset = 0;
|
||||
structNode.collapsed = true;
|
||||
|
||||
// Enum: store members directly on the node, no child fields
|
||||
if (ps.keyword == QStringLiteral("enum")) {
|
||||
structNode.enumMembers = ps.enumValues;
|
||||
int idx = tree.addNode(structNode);
|
||||
uint64_t nodeId = tree.nodes[idx].id;
|
||||
if (!ps.name.isEmpty())
|
||||
classIds[ps.name] = nodeId;
|
||||
continue;
|
||||
}
|
||||
|
||||
int structIdx = tree.addNode(structNode);
|
||||
uint64_t structId = tree.nodes[structIdx].id;
|
||||
classIds[ps.name] = structId;
|
||||
|
||||
int computedOffset = 0;
|
||||
|
||||
for (const auto& field : ps.fields) {
|
||||
// Skip bitfields
|
||||
if (field.bitfieldWidth >= 0) continue;
|
||||
|
||||
int fieldOffset;
|
||||
if (useCommentOffsets && field.commentOffset >= 0)
|
||||
fieldOffset = field.commentOffset;
|
||||
else
|
||||
fieldOffset = computedOffset;
|
||||
|
||||
// Resolve type
|
||||
auto typeIt = typeTable.find(field.typeName);
|
||||
bool knownType = typeIt != typeTable.end();
|
||||
|
||||
// Pointer field
|
||||
if (field.isPointer) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
|
||||
// If target is not void and not a primitive, defer resolution
|
||||
if (!field.pointerTarget.isEmpty() &&
|
||||
field.pointerTarget != QStringLiteral("void")) {
|
||||
pendingRefs.append({nodeId, field.pointerTarget});
|
||||
}
|
||||
|
||||
computedOffset = fieldOffset + 8; // pointer size
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine base type info
|
||||
NodeKind baseKind = NodeKind::Hex8;
|
||||
int baseSize = 1;
|
||||
bool isStructType = false;
|
||||
|
||||
if (knownType) {
|
||||
baseKind = typeIt->kind;
|
||||
baseSize = typeIt->size;
|
||||
} else {
|
||||
// Unknown type = assume struct reference
|
||||
isStructType = true;
|
||||
}
|
||||
|
||||
// Padding fields: name-based detection
|
||||
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
|
||||
int totalSize = baseSize;
|
||||
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
|
||||
emitHexPadding(tree, structId, fieldOffset, totalSize);
|
||||
computedOffset = fieldOffset + totalSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Array fields
|
||||
if (!field.arraySizes.isEmpty() && !isStructType) {
|
||||
int firstDim = field.arraySizes.value(0, 1);
|
||||
if (firstDim <= 0) firstDim = 1;
|
||||
|
||||
// Special: char[N] -> UTF8
|
||||
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
||||
field.typeName == QStringLiteral("char")) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF8;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special: wchar_t[N] -> UTF16
|
||||
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF16;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim * 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special: float[2] -> Vec2, float[3] -> Vec3, float[4] -> Vec4
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
|
||||
if (firstDim == 2) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Vec2;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 8;
|
||||
continue;
|
||||
}
|
||||
if (firstDim == 3) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Vec3;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 12;
|
||||
continue;
|
||||
}
|
||||
if (firstDim == 4) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Vec4;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 16;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Special: float[4][4] -> Mat4x4
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
|
||||
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Mat4x4;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 64;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generic array
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = baseKind;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * baseSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Struct-type field (embedded struct or array of structs)
|
||||
if (isStructType) {
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
// Array of structs
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = NodeKind::Struct;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, field.typeName});
|
||||
|
||||
// For computed offsets: we don't know struct size yet, use 0
|
||||
// The offset will be approximate for unknown struct sizes
|
||||
if (!useCommentOffsets) {
|
||||
// Try to estimate from same-file structs
|
||||
// Can't know size yet since we may not have parsed it
|
||||
// Just advance by 0 (will be corrected by comment offsets if present)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Embedded struct
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, field.typeName});
|
||||
// Don't advance computed offset for unknown struct size
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simple primitive field
|
||||
Node n;
|
||||
n.kind = baseKind;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + baseSize;
|
||||
}
|
||||
buildFields(ctx, structId, 0, ps.fields);
|
||||
|
||||
// Apply static_assert size: add tail padding if needed
|
||||
auto sizeIt = parser.sizeAsserts.find(ps.name);
|
||||
@@ -1056,7 +1324,6 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
auto it = classIds.find(ref.className);
|
||||
if (it != classIds.end()) {
|
||||
tree.nodes[nodeIdx].refId = it.value();
|
||||
tree.invalidateIdCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ namespace rcx {
|
||||
// Supports two modes (auto-detected):
|
||||
// 1. With comment offsets (// 0xNN) - trusts the offset values
|
||||
// 2. Without comment offsets - computes offsets from type sizes
|
||||
// pointerSize: 4 for 32-bit targets, 8 for 64-bit (default).
|
||||
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr,
|
||||
int pointerSize = 8);
|
||||
|
||||
} // namespace rcx
|
||||
@@ -66,7 +66,8 @@ struct PluginProcessInfo {
|
||||
QString name;
|
||||
QString path;
|
||||
QIcon icon;
|
||||
|
||||
bool is32Bit = false;
|
||||
|
||||
PluginProcessInfo() : pid(0) {}
|
||||
PluginProcessInfo(uint32_t p, const QString& n, const QString& pth = QString(), const QIcon& i = QIcon())
|
||||
: pid(p), name(n), path(pth), icon(i) {}
|
||||
|
||||
13
src/macos_titlebar.h
Normal file
13
src/macos_titlebar.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct Theme;
|
||||
|
||||
// Apply macOS native title bar color to match the theme.
|
||||
// No-op on non-macOS platforms (implementation is platform-specific).
|
||||
void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
|
||||
|
||||
} // namespace rcx
|
||||
43
src/macos_titlebar.mm
Normal file
43
src/macos_titlebar.mm
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "macos_titlebar.h"
|
||||
#include "themes/theme.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <QColor>
|
||||
#include <QWidget>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
static NSColor* toNSColor(const QColor& color) {
|
||||
return [NSColor colorWithCalibratedRed:color.redF()
|
||||
green:color.greenF()
|
||||
blue:color.blueF()
|
||||
alpha:color.alphaF()];
|
||||
}
|
||||
|
||||
void applyMacTitleBarTheme(QWidget* window, const Theme& theme) {
|
||||
if (!window) return;
|
||||
|
||||
// Ensure native window is created.
|
||||
window->winId();
|
||||
|
||||
auto* nsView = reinterpret_cast<NSView*>(window->winId());
|
||||
if (!nsView) return;
|
||||
|
||||
NSWindow* nsWindow = [nsView window];
|
||||
if (!nsWindow) return;
|
||||
|
||||
// Keep native traffic lights while tinting the title bar to the theme.
|
||||
// Match the title text contrast by selecting the appropriate system appearance.
|
||||
const qreal luminance =
|
||||
0.2126 * theme.background.redF() +
|
||||
0.7152 * theme.background.greenF() +
|
||||
0.0722 * theme.background.blueF();
|
||||
const bool isLight = luminance >= 0.5;
|
||||
[nsWindow setAppearance:[NSAppearance appearanceNamed:
|
||||
(isLight ? NSAppearanceNameAqua : NSAppearanceNameDarkAqua)]];
|
||||
[nsWindow setTitlebarAppearsTransparent:YES];
|
||||
[nsWindow setTitleVisibility:NSWindowTitleVisible];
|
||||
[nsWindow setBackgroundColor:toNSColor(theme.background)];
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
1626
src/main.cpp
1626
src/main.cpp
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
#include "controller.h"
|
||||
#include "titlebar.h"
|
||||
#include "pluginmanager.h"
|
||||
#include "scannerpanel.h"
|
||||
#include <QMainWindow>
|
||||
#include <QMdiArea>
|
||||
#include <QMdiSubWindow>
|
||||
@@ -11,18 +12,25 @@
|
||||
#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
|
||||
friend class McpBridge;
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
private slots:
|
||||
void newClass();
|
||||
@@ -51,11 +59,17 @@ private slots:
|
||||
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(const QString& classKeyword = QString());
|
||||
QMdiSubWindow* project_open(const QString& path = {});
|
||||
@@ -66,17 +80,31 @@ 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;
|
||||
QMenuBar* m_menuBar = nullptr;
|
||||
bool m_menuBarTitleCase = false;
|
||||
QWidget* m_borderOverlay = nullptr;
|
||||
PluginManager m_pluginManager;
|
||||
McpBridge* m_mcp = nullptr;
|
||||
QAction* m_mcpAction = nullptr;
|
||||
QAction* m_removeSplitAction = nullptr;
|
||||
QMenu* m_sourceMenu = nullptr;
|
||||
QMenu* m_recentFilesMenu = nullptr;
|
||||
|
||||
struct SplitPane {
|
||||
QTabWidget* tabWidget = nullptr;
|
||||
RcxEditor* editor = nullptr;
|
||||
QsciScintilla* rendered = nullptr;
|
||||
QLineEdit* findBar = nullptr;
|
||||
QWidget* findContainer = nullptr;
|
||||
QWidget* renderedContainer = nullptr;
|
||||
ViewMode viewMode = VM_Reclass;
|
||||
uint64_t lastRenderedRootId = 0;
|
||||
};
|
||||
@@ -89,11 +117,16 @@ private:
|
||||
int activePaneIdx = 0;
|
||||
};
|
||||
QMap<QMdiSubWindow*, TabState> m_tabs;
|
||||
|
||||
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
||||
void rebuildAllDocs();
|
||||
|
||||
void createMenus();
|
||||
void applyMenuBarTitleCase(bool titleCase);
|
||||
void createStatusBar();
|
||||
void showPluginsDialog();
|
||||
void populateSourceMenu();
|
||||
void addRecentFile(const QString& path);
|
||||
void updateRecentFilesMenu();
|
||||
QIcon makeIcon(const QString& svgPath);
|
||||
|
||||
RcxController* activeController() const;
|
||||
@@ -111,20 +144,31 @@ 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);
|
||||
|
||||
// Scanner dock
|
||||
QDockWidget* m_scannerDock = nullptr;
|
||||
ScannerPanel* m_scannerPanel = nullptr;
|
||||
QLabel* m_scanDockTitle = nullptr;
|
||||
QToolButton* m_scanDockCloseBtn = nullptr;
|
||||
void createScannerDock();
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "generator.h"
|
||||
#include "mainwindow.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QSettings>
|
||||
#include <QDebug>
|
||||
#include <cstring>
|
||||
|
||||
@@ -170,9 +171,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 +218,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."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
@@ -240,7 +256,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
"insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0}. "
|
||||
"change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. "
|
||||
"change_offset: {op:'change_offset', nodeId:'ID', offset:16}. "
|
||||
"change_base: {op:'change_base', baseAddress:'0x400000'}. "
|
||||
"change_base: {op:'change_base', baseAddress:'0x400000', formula:'[0x233CA80]'} — formula is optional, enables auto-resolve on provider attach. "
|
||||
"change_struct_type: {op:'change_struct_type', nodeId:'ID', structTypeName:'Name'}. "
|
||||
"change_class_keyword: {op:'change_class_keyword', nodeId:'ID', classKeyword:'class'}. "
|
||||
"change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. "
|
||||
@@ -343,7 +359,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{
|
||||
@@ -357,6 +374,46 @@ 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)."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
|
||||
// 9. node.history
|
||||
tools.append(QJsonObject{
|
||||
{"name", "node.history"},
|
||||
{"description", "Returns timestamped value change history (up to 10 entries) "
|
||||
"for specified nodes. Requires live provider with value tracking enabled."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"nodeIds", QJsonObject{{"type", "array"},
|
||||
{"items", QJsonObject{{"type", "string"}}},
|
||||
{"description", "Array of node IDs to get history for."}}},
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index. Omit for active tab."}}}
|
||||
}},
|
||||
{"required", QJsonArray{"nodeIds"}}
|
||||
}}
|
||||
});
|
||||
|
||||
return okReply(id, QJsonObject{{"tools", tools}});
|
||||
}
|
||||
|
||||
@@ -368,6 +425,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);
|
||||
@@ -376,8 +437,12 @@ 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 if (toolName == "node.history") result = toolNodeHistory(args);
|
||||
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||
|
||||
m_mainWindow->clearMcpStatus();
|
||||
|
||||
return okReply(id, result);
|
||||
}
|
||||
|
||||
@@ -436,6 +501,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();
|
||||
|
||||
@@ -481,6 +549,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) {
|
||||
@@ -489,12 +558,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;
|
||||
@@ -502,13 +574,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)
|
||||
@@ -520,6 +626,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;
|
||||
}
|
||||
|
||||
@@ -660,8 +770,10 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
}
|
||||
else if (opType == "change_base") {
|
||||
uint64_t newBase = op.value("baseAddress").toString().toULongLong(nullptr, 16);
|
||||
QString oldFormula = tree.baseAddressFormula;
|
||||
QString newFormula = op.value("formula").toString();
|
||||
doc->undoStack.push(new RcxCommand(ctrl,
|
||||
cmd::ChangeBase{tree.baseAddress, newBase}));
|
||||
cmd::ChangeBase{tree.baseAddress, newBase, oldFormula, newFormula}));
|
||||
applied++;
|
||||
}
|
||||
else if (opType == "change_struct_type") {
|
||||
@@ -956,7 +1068,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);
|
||||
@@ -1004,7 +1116,25 @@ 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);
|
||||
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
||||
QString code;
|
||||
if (!nodeIdStr.isEmpty()) {
|
||||
// Per-struct export
|
||||
uint64_t nid = nodeIdStr.toULongLong();
|
||||
code = renderCpp(doc->tree, nid, aliases, asserts);
|
||||
if (code.isEmpty())
|
||||
return makeTextResult("Node not found or not a struct: " + nodeIdStr, true);
|
||||
} else {
|
||||
code = renderCppAll(doc->tree, aliases, asserts);
|
||||
}
|
||||
// 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") {
|
||||
@@ -1053,6 +1183,107 @@ 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)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Tool: node.history — return timestamped value history for nodes
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolNodeHistory(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab.", true);
|
||||
|
||||
const auto& histMap = tab->ctrl->valueHistory();
|
||||
QJsonArray requestedIds = args.value("nodeIds").toArray();
|
||||
if (requestedIds.isEmpty())
|
||||
return makeTextResult("nodeIds array is required.", true);
|
||||
|
||||
QJsonObject result;
|
||||
for (const auto& idVal : requestedIds) {
|
||||
QString idStr = idVal.toString();
|
||||
uint64_t nodeId = idStr.toULongLong();
|
||||
auto it = histMap.find(nodeId);
|
||||
QJsonArray entries;
|
||||
if (it != histMap.end()) {
|
||||
it->forEachWithTime([&](const QString& val, qint64 msec) {
|
||||
QJsonObject entry;
|
||||
entry.insert(QStringLiteral("value"), val);
|
||||
entry.insert(QStringLiteral("timestamp"), msec);
|
||||
entries.append(entry);
|
||||
});
|
||||
}
|
||||
QJsonObject nodeResult;
|
||||
nodeResult.insert(QStringLiteral("entries"), entries);
|
||||
nodeResult.insert(QStringLiteral("heatLevel"), it != histMap.end() ? it->heatLevel() : 0);
|
||||
nodeResult.insert(QStringLiteral("uniqueCount"), it != histMap.end() ? it->uniqueCount() : 0);
|
||||
result.insert(idStr, nodeResult);
|
||||
}
|
||||
return makeTextResult(QString::fromUtf8(
|
||||
QJsonDocument(result).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Notifications (call from MainWindow/Controller hooks)
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -58,6 +58,8 @@ private:
|
||||
QJsonObject toolHexWrite(const QJsonObject& args);
|
||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
QJsonObject toolNodeHistory(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
|
||||
@@ -170,6 +170,14 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
||||
generatorLayout->setContentsMargins(0, 0, 0, 0);
|
||||
generatorLayout->setSpacing(8);
|
||||
|
||||
auto* cppGroup = new QGroupBox("C++ Header");
|
||||
auto* cppLayout = new QVBoxLayout(cppGroup);
|
||||
m_assertCheck = new QCheckBox("Emit static_assert size checks");
|
||||
m_assertCheck->setChecked(current.generatorAsserts);
|
||||
cppLayout->addWidget(m_assertCheck);
|
||||
generatorLayout->addWidget(cppGroup);
|
||||
|
||||
generatorLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(generatorPage); // index 2
|
||||
@@ -208,6 +216,7 @@ OptionsResult OptionsDialog::result() const {
|
||||
r.safeMode = m_safeModeCheck->isChecked();
|
||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||
r.refreshMs = m_refreshSpin->value();
|
||||
r.generatorAsserts = m_assertCheck->isChecked();
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@ struct OptionsResult {
|
||||
bool menuBarTitleCase = true;
|
||||
bool showIcon = false;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = false;
|
||||
bool autoStartMcp = true;
|
||||
int refreshMs = 660;
|
||||
bool generatorAsserts = false;
|
||||
};
|
||||
|
||||
class OptionsDialog : public QDialog {
|
||||
@@ -41,6 +42,7 @@ private:
|
||||
QCheckBox* m_safeModeCheck = nullptr;
|
||||
QCheckBox* m_autoMcpCheck = nullptr;
|
||||
QSpinBox* m_refreshSpin = nullptr;
|
||||
QCheckBox* m_assertCheck = nullptr;
|
||||
|
||||
// searchable keywords per leaf tree item
|
||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <QMessageBox>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QMenu>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -27,22 +31,9 @@ ProcessPicker::ProcessPicker(QWidget *parent)
|
||||
, m_useCustomList(false)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column - fixed width
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column - fixed width
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true); // Path column - fills remaining space
|
||||
ui->processTable->setWordWrap(false); // Disable word wrap for single-line display
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft); // Elide from left (show end of path)
|
||||
|
||||
// Connect signals
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Initial process enumeration
|
||||
initUi();
|
||||
refreshProcessList();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget *parent)
|
||||
@@ -51,23 +42,102 @@ ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget
|
||||
, m_useCustomList(true)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80);
|
||||
ui->processTable->setColumnWidth(1, 200);
|
||||
initUi();
|
||||
ui->refreshButton->setVisible(false);
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
void ProcessPicker::initUi()
|
||||
{
|
||||
// Table configuration
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true);
|
||||
ui->processTable->setWordWrap(false);
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft);
|
||||
|
||||
// Connect signals (no refresh button for custom lists)
|
||||
ui->refreshButton->setVisible(false);
|
||||
ui->processTable->setShowGrid(false);
|
||||
ui->processTable->verticalHeader()->setDefaultSectionSize(fontMetrics().height() + 6);
|
||||
|
||||
// Signal connections
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Use custom process list
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
|
||||
// Derive theme colors from the global palette (set by applyGlobalTheme)
|
||||
QPalette pal = qApp->palette();
|
||||
QString bg = pal.color(QPalette::Base).name();
|
||||
QString text = pal.color(QPalette::Text).name();
|
||||
QString hover = pal.color(QPalette::Mid).name();
|
||||
QString surface = pal.color(QPalette::AlternateBase).name();
|
||||
QString button = pal.color(QPalette::Button).name();
|
||||
QString highlight= pal.color(QPalette::Highlight).name();
|
||||
QString border = pal.color(QPalette::Mid).darker(120).name();
|
||||
QString mutedText= pal.color(QPalette::Disabled, QPalette::WindowText).name();
|
||||
QString hoverDk = pal.color(QPalette::Mid).darker(130).name();
|
||||
|
||||
ui->processTable->setStyleSheet(QStringLiteral(
|
||||
"QTableWidget { background: %1; color: %2; border: none; }"
|
||||
"QTableWidget::item { padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }")
|
||||
.arg(bg, text, hover));
|
||||
|
||||
ui->processTable->horizontalHeader()->setStyleSheet(QStringLiteral(
|
||||
"QHeaderView::section { background: %1; color: %2; border: none;"
|
||||
" padding: 4px 6px; text-align: left; }")
|
||||
.arg(surface, text));
|
||||
ui->processTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
ui->filterEdit->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
|
||||
"QLineEdit:focus { border-color: %4; }")
|
||||
.arg(bg, text, border, highlight));
|
||||
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
|
||||
"QPushButton:hover { background: %4; }"
|
||||
"QPushButton:pressed { background: %5; }"
|
||||
"QPushButton:disabled { color: %6; }")
|
||||
.arg(button, text, border, hover, hoverDk, mutedText);
|
||||
ui->refreshButton->setStyleSheet(btnStyle);
|
||||
ui->attachButton->setStyleSheet(btnStyle);
|
||||
ui->cancelButton->setStyleSheet(btnStyle);
|
||||
|
||||
// Right-click context menu
|
||||
ui->processTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(ui->processTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
int row = ui->processTable->rowAt(pos.y());
|
||||
if (row < 0) return;
|
||||
auto* pidItem = ui->processTable->item(row, 0);
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
auto* pathItem = ui->processTable->item(row, 2);
|
||||
if (!pidItem || !nameItem) return;
|
||||
|
||||
QString pid = QString::number(pidItem->data(Qt::EditRole).toUInt());
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
QString path = pathItem ? pathItem->text() : QString();
|
||||
|
||||
QMenu menu;
|
||||
auto* copyPid = menu.addAction(QStringLiteral("Copy PID"));
|
||||
auto* copyName = menu.addAction(QStringLiteral("Copy Name"));
|
||||
QAction* copyPath = nullptr;
|
||||
if (!path.isEmpty())
|
||||
copyPath = menu.addAction(QStringLiteral("Copy Path"));
|
||||
|
||||
auto* chosen = menu.exec(ui->processTable->viewport()->mapToGlobal(pos));
|
||||
if (chosen == copyPid)
|
||||
QApplication::clipboard()->setText(pid);
|
||||
else if (chosen == copyName)
|
||||
QApplication::clipboard()->setText(name);
|
||||
else if (copyPath && chosen == copyPath)
|
||||
QApplication::clipboard()->setText(path);
|
||||
});
|
||||
|
||||
// Auto-focus filter for immediate typing
|
||||
ui->filterEdit->setFocus();
|
||||
}
|
||||
|
||||
ProcessPicker::~ProcessPicker()
|
||||
@@ -97,28 +167,31 @@ void ProcessPicker::onProcessSelected()
|
||||
{
|
||||
auto* item = ui->processTable->currentItem();
|
||||
if (!item) return;
|
||||
|
||||
|
||||
int row = item->row();
|
||||
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
|
||||
m_selectedName = ui->processTable->item(row, 1)->text();
|
||||
|
||||
// Use original name stored in UserRole (without architecture suffix)
|
||||
QVariant origName = ui->processTable->item(row, 1)->data(Qt::UserRole);
|
||||
m_selectedName = origName.isValid() ? origName.toString()
|
||||
: ui->processTable->item(row, 1)->text();
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void ProcessPicker::enumerateProcesses()
|
||||
{
|
||||
QList<ProcessInfo> processes;
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||
QMessageBox::warning(this, "Error", "Failed to enumerate processes.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
PROCESSENTRY32W pe32;
|
||||
pe32.dwSize = sizeof(PROCESSENTRY32W);
|
||||
|
||||
|
||||
if (Process32FirstW(snapshot, &pe32))
|
||||
{
|
||||
do
|
||||
@@ -126,10 +199,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
ProcessInfo info;
|
||||
info.pid = pe32.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(pe32.szExeFile);
|
||||
|
||||
// Try to get full path and extract icon
|
||||
// If we can't open a process with PROCESS_QUERY_LIMITED_INFORMATION then
|
||||
// we for sure can't access their memory. - Skip in this case
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
|
||||
if (hProcess)
|
||||
{
|
||||
@@ -140,7 +210,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
GetModuleFileNameExW(hProcess, nullptr, path, pathLen))
|
||||
{
|
||||
info.path = QString::fromWCharArray(path);
|
||||
|
||||
|
||||
// Extract icon from executable
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
|
||||
@@ -158,11 +228,16 @@ void ProcessPicker::enumerateProcesses()
|
||||
{
|
||||
info.path = "";
|
||||
}
|
||||
// Detect 32-bit (WoW64) process
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(hProcess, &isWow64) && isWow64)
|
||||
info.is32Bit = true;
|
||||
|
||||
CloseHandle(hProcess);
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
|
||||
|
||||
} while (Process32NextW(snapshot, &pe32));
|
||||
}
|
||||
|
||||
@@ -204,6 +279,16 @@ void ProcessPicker::enumerateProcesses()
|
||||
info.name = procName;
|
||||
info.path = resolvedPath;
|
||||
info.icon = defaultIcon;
|
||||
|
||||
// Detect 32-bit ELF process
|
||||
QFile exeFile(exePath);
|
||||
if (exeFile.open(QIODevice::ReadOnly)) {
|
||||
QByteArray header = exeFile.read(5);
|
||||
if (header.size() >= 5 && header[4] == 1) // ELFCLASS32
|
||||
info.is32Bit = true;
|
||||
exeFile.close();
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#else
|
||||
@@ -227,11 +312,16 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
|
||||
pidItem->setData(Qt::EditRole, (int)proc.pid);
|
||||
ui->processTable->setItem(i, 0, pidItem);
|
||||
|
||||
// Name column with icon
|
||||
auto* nameItem = new QTableWidgetItem(proc.name);
|
||||
// Name column with icon and architecture indicator
|
||||
QString displayName = proc.is32Bit
|
||||
? proc.name + QStringLiteral(" (32-bit)")
|
||||
: proc.name;
|
||||
auto* nameItem = new QTableWidgetItem(displayName);
|
||||
if (!proc.icon.isNull()) {
|
||||
nameItem->setIcon(proc.icon);
|
||||
}
|
||||
// Store original name for selectedProcessName()
|
||||
nameItem->setData(Qt::UserRole, proc.name);
|
||||
ui->processTable->setItem(i, 1, nameItem);
|
||||
|
||||
// Path column with tooltip for full path
|
||||
@@ -269,3 +359,22 @@ void ProcessPicker::applyFilter()
|
||||
|
||||
populateTable(filtered);
|
||||
}
|
||||
|
||||
void ProcessPicker::selectPreferredProcess()
|
||||
{
|
||||
// Try to select the last-attached process if it's in the list
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QString lastProc = s.value("lastAttachedProcess").toString();
|
||||
if (lastProc.isEmpty()) return;
|
||||
|
||||
for (int row = 0; row < ui->processTable->rowCount(); ++row) {
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
if (!nameItem) continue;
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
if (name.compare(lastProc, Qt::CaseInsensitive) == 0) {
|
||||
ui->processTable->selectRow(row);
|
||||
ui->processTable->scrollToItem(nameItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ struct ProcessInfo {
|
||||
QString name;
|
||||
QString path;
|
||||
QIcon icon;
|
||||
bool is32Bit = false;
|
||||
};
|
||||
|
||||
class ProcessPicker : public QDialog
|
||||
@@ -34,9 +35,11 @@ private slots:
|
||||
void filterProcesses(const QString& text);
|
||||
|
||||
private:
|
||||
void initUi();
|
||||
void enumerateProcesses();
|
||||
void populateTable(const QList<ProcessInfo>& processes);
|
||||
void applyFilter();
|
||||
void selectPreferredProcess();
|
||||
|
||||
Ui::ProcessPicker *ui;
|
||||
uint32_t m_selectedPid = 0;
|
||||
|
||||
@@ -127,22 +127,6 @@
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>attachButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ProcessPicker</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>600</x>
|
||||
<y>470</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>350</x>
|
||||
<y>250</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>cancelButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
#pragma once
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct MemoryRegion {
|
||||
uint64_t base = 0;
|
||||
uint64_t size = 0;
|
||||
bool readable = true;
|
||||
bool writable = false;
|
||||
bool executable = false;
|
||||
QString moduleName;
|
||||
};
|
||||
|
||||
class Provider {
|
||||
public:
|
||||
virtual ~Provider() = default;
|
||||
@@ -33,6 +43,10 @@ public:
|
||||
// Examples: "File", "Process", "Socket"
|
||||
virtual QString kind() const { return QStringLiteral("File"); }
|
||||
|
||||
// Native pointer size of the target (4 for 32-bit, 8 for 64-bit).
|
||||
// Providers should override this to report the target's architecture.
|
||||
virtual int pointerSize() const { return 8; }
|
||||
|
||||
// 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.
|
||||
@@ -47,6 +61,18 @@ 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;
|
||||
}
|
||||
|
||||
// Enumerate committed/readable memory regions.
|
||||
// Used by the scan engine to know what address ranges to scan.
|
||||
// Default: returns empty (scan engine falls back to [0, size())).
|
||||
virtual QVector<MemoryRegion> enumerateRegions() const { return {}; }
|
||||
|
||||
// --- 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;
|
||||
|
||||
@@ -51,5 +51,14 @@
|
||||
<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="symbol-class.svg">vsicons/symbol-class.svg</file>
|
||||
<file alias="symbol-variable.svg">vsicons/symbol-variable.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>
|
||||
<file alias="search.svg">vsicons/search.svg</file>
|
||||
<file alias="regex.svg">vsicons/regex.svg</file>
|
||||
<file alias="refresh.svg">vsicons/refresh.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
659
src/scanner.cpp
Normal file
659
src/scanner.cpp
Normal file
@@ -0,0 +1,659 @@
|
||||
#include "scanner.h"
|
||||
#include <QtConcurrent>
|
||||
#include <QMetaObject>
|
||||
#include <QElapsedTimer>
|
||||
#include <QDebug>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Pattern parsing ──
|
||||
|
||||
static int hexVal(QChar c) {
|
||||
ushort u = c.unicode();
|
||||
if (u >= '0' && u <= '9') return u - '0';
|
||||
if (u >= 'a' && u <= 'f') return u - 'a' + 10;
|
||||
if (u >= 'A' && u <= 'F') return u - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool parseSignature(const QString& input, QByteArray& pattern, QByteArray& mask,
|
||||
QString* errorMsg)
|
||||
{
|
||||
pattern.clear();
|
||||
mask.clear();
|
||||
|
||||
QString trimmed = input.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Empty pattern");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for C-style: \xAB\xCD
|
||||
if (trimmed.startsWith(QStringLiteral("\\x"))) {
|
||||
QStringList parts = trimmed.split(QStringLiteral("\\x"), Qt::SkipEmptyParts);
|
||||
for (const QString& part : parts) {
|
||||
if (part.size() != 2) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid C-style byte: \\x%1").arg(part);
|
||||
return false;
|
||||
}
|
||||
int hi = hexVal(part[0]);
|
||||
int lo = hexVal(part[1]);
|
||||
if (hi < 0 || lo < 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex char in: \\x%1").arg(part);
|
||||
return false;
|
||||
}
|
||||
pattern.append(char((hi << 4) | lo));
|
||||
mask.append(char(0xFF));
|
||||
}
|
||||
return !pattern.isEmpty();
|
||||
}
|
||||
|
||||
// Space-separated or packed hex
|
||||
bool hasSpaces = trimmed.contains(' ');
|
||||
|
||||
if (hasSpaces) {
|
||||
QStringList tokens = trimmed.split(' ', Qt::SkipEmptyParts);
|
||||
for (const QString& tok : tokens) {
|
||||
if (tok == QStringLiteral("??") || tok == QStringLiteral("?")) {
|
||||
pattern.append(char(0));
|
||||
mask.append(char(0));
|
||||
} else if (tok.size() == 2) {
|
||||
int hi = hexVal(tok[0]);
|
||||
int lo = hexVal(tok[1]);
|
||||
if (hi < 0 || lo < 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex byte: %1").arg(tok);
|
||||
return false;
|
||||
}
|
||||
pattern.append(char((hi << 4) | lo));
|
||||
mask.append(char(0xFF));
|
||||
} else {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid token: %1 (expected 2 hex chars or wildcards)").arg(tok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Packed: "488B??05"
|
||||
if (trimmed.size() % 2 != 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Odd number of characters in packed pattern");
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < trimmed.size(); i += 2) {
|
||||
QChar c0 = trimmed[i], c1 = trimmed[i + 1];
|
||||
if ((c0 == '?' && c1 == '?')) {
|
||||
pattern.append(char(0));
|
||||
mask.append(char(0));
|
||||
} else {
|
||||
int hi = hexVal(c0);
|
||||
int lo = hexVal(c1);
|
||||
if (hi < 0 || lo < 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid hex chars at position %1: %2%3")
|
||||
.arg(i).arg(c0).arg(c1);
|
||||
return false;
|
||||
}
|
||||
pattern.append(char((hi << 4) | lo));
|
||||
mask.append(char(0xFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pattern.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Empty pattern after parsing");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Value serialization ──
|
||||
|
||||
template<typename T>
|
||||
static void appendLE(QByteArray& out, T val) {
|
||||
out.append(reinterpret_cast<const char*>(&val), sizeof(T));
|
||||
}
|
||||
|
||||
bool serializeValue(ValueType type, const QString& input,
|
||||
QByteArray& pattern, QByteArray& mask,
|
||||
QString* errorMsg)
|
||||
{
|
||||
pattern.clear();
|
||||
mask.clear();
|
||||
|
||||
QString trimmed = input.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Empty value");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
|
||||
switch (type) {
|
||||
case ValueType::Int8: {
|
||||
int v = trimmed.toInt(&ok);
|
||||
if (!ok || v < -128 || v > 127) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid int8 value");
|
||||
return false;
|
||||
}
|
||||
appendLE<int8_t>(pattern, (int8_t)v);
|
||||
break;
|
||||
}
|
||||
case ValueType::Int16: {
|
||||
int v = trimmed.toInt(&ok);
|
||||
if (!ok || v < -32768 || v > 32767) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid int16 value");
|
||||
return false;
|
||||
}
|
||||
appendLE<int16_t>(pattern, (int16_t)v);
|
||||
break;
|
||||
}
|
||||
case ValueType::Int32: {
|
||||
int v = trimmed.toInt(&ok);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid int32 value");
|
||||
return false;
|
||||
}
|
||||
appendLE<int32_t>(pattern, (int32_t)v);
|
||||
break;
|
||||
}
|
||||
case ValueType::Int64: {
|
||||
qlonglong v = trimmed.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid int64 value");
|
||||
return false;
|
||||
}
|
||||
appendLE<int64_t>(pattern, (int64_t)v);
|
||||
break;
|
||||
}
|
||||
case ValueType::UInt8: {
|
||||
uint v = trimmed.toUInt(&ok);
|
||||
if (!ok || v > 255) {
|
||||
// Try hex
|
||||
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
||||
v = trimmed.toUInt(&ok, 16);
|
||||
if (!ok || v > 255) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint8 value");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
appendLE<uint8_t>(pattern, (uint8_t)v);
|
||||
break;
|
||||
}
|
||||
case ValueType::UInt16: {
|
||||
uint v = trimmed.toUInt(&ok);
|
||||
if (!ok || v > 65535) {
|
||||
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
||||
v = trimmed.toUInt(&ok, 16);
|
||||
if (!ok || v > 65535) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint16 value");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
appendLE<uint16_t>(pattern, (uint16_t)v);
|
||||
break;
|
||||
}
|
||||
case ValueType::UInt32: {
|
||||
quint32 v = trimmed.toULong(&ok);
|
||||
if (!ok) {
|
||||
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
||||
v = trimmed.toULong(&ok, 16);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint32 value");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
appendLE<uint32_t>(pattern, v);
|
||||
break;
|
||||
}
|
||||
case ValueType::UInt64: {
|
||||
quint64 v = trimmed.toULongLong(&ok);
|
||||
if (!ok) {
|
||||
if (trimmed.startsWith("0x", Qt::CaseInsensitive))
|
||||
v = trimmed.toULongLong(&ok, 16);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid uint64 value");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
appendLE<uint64_t>(pattern, v);
|
||||
break;
|
||||
}
|
||||
case ValueType::Float: {
|
||||
float v = trimmed.toFloat(&ok);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid float value");
|
||||
return false;
|
||||
}
|
||||
appendLE<float>(pattern, v);
|
||||
break;
|
||||
}
|
||||
case ValueType::Double: {
|
||||
double v = trimmed.toDouble(&ok);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid double value");
|
||||
return false;
|
||||
}
|
||||
appendLE<double>(pattern, v);
|
||||
break;
|
||||
}
|
||||
case ValueType::Vec2: {
|
||||
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
if (parts.size() != 2) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Vec2 requires 2 space-separated floats");
|
||||
return false;
|
||||
}
|
||||
for (const QString& p : parts) {
|
||||
float v = p.toFloat(&ok);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec2: %1").arg(p);
|
||||
return false;
|
||||
}
|
||||
appendLE<float>(pattern, v);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ValueType::Vec3: {
|
||||
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
if (parts.size() != 3) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Vec3 requires 3 space-separated floats");
|
||||
return false;
|
||||
}
|
||||
for (const QString& p : parts) {
|
||||
float v = p.toFloat(&ok);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec3: %1").arg(p);
|
||||
return false;
|
||||
}
|
||||
appendLE<float>(pattern, v);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ValueType::Vec4: {
|
||||
QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
if (parts.size() != 4) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Vec4 requires 4 space-separated floats");
|
||||
return false;
|
||||
}
|
||||
for (const QString& p : parts) {
|
||||
float v = p.toFloat(&ok);
|
||||
if (!ok) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec4: %1").arg(p);
|
||||
return false;
|
||||
}
|
||||
appendLE<float>(pattern, v);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ValueType::UTF8: {
|
||||
QByteArray encoded = trimmed.toUtf8();
|
||||
if (encoded.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-8 string");
|
||||
return false;
|
||||
}
|
||||
pattern = encoded;
|
||||
break;
|
||||
}
|
||||
case ValueType::UTF16: {
|
||||
// UTF-16LE encoding
|
||||
for (int i = 0; i < trimmed.size(); i++) {
|
||||
ushort u = trimmed[i].unicode();
|
||||
appendLE<uint16_t>(pattern, u);
|
||||
}
|
||||
if (pattern.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-16 string");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ValueType::HexBytes: {
|
||||
// Parse hex bytes (like signature but no wildcards)
|
||||
QByteArray dummyMask;
|
||||
if (!parseSignature(trimmed, pattern, dummyMask, errorMsg))
|
||||
return false;
|
||||
// HexBytes = exact match, no wildcards
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set mask to all 0xFF (exact match) for value scans
|
||||
mask.fill(char(0xFF), pattern.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
int naturalAlignment(ValueType type) {
|
||||
switch (type) {
|
||||
case ValueType::Int8:
|
||||
case ValueType::UInt8:
|
||||
case ValueType::UTF8:
|
||||
case ValueType::HexBytes:
|
||||
return 1;
|
||||
case ValueType::Int16:
|
||||
case ValueType::UInt16:
|
||||
case ValueType::UTF16:
|
||||
return 2;
|
||||
case ValueType::Int32:
|
||||
case ValueType::UInt32:
|
||||
case ValueType::Float:
|
||||
case ValueType::Vec2:
|
||||
case ValueType::Vec3:
|
||||
case ValueType::Vec4:
|
||||
return 4;
|
||||
case ValueType::Int64:
|
||||
case ValueType::UInt64:
|
||||
case ValueType::Double:
|
||||
return 8;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ── Scan engine ──
|
||||
|
||||
ScanEngine::ScanEngine(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
qRegisterMetaType<QVector<ScanResult>>("QVector<rcx::ScanResult>");
|
||||
}
|
||||
|
||||
bool ScanEngine::isRunning() const {
|
||||
return m_watcher && m_watcher->isRunning();
|
||||
}
|
||||
|
||||
void ScanEngine::abort() {
|
||||
m_abort.store(true);
|
||||
}
|
||||
|
||||
void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& req) {
|
||||
if (isRunning()) return;
|
||||
|
||||
if (req.pattern.isEmpty()) {
|
||||
emit error(QStringLiteral("Empty pattern"));
|
||||
return;
|
||||
}
|
||||
if (req.pattern.size() != req.mask.size()) {
|
||||
emit error(QStringLiteral("Pattern and mask size mismatch"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_abort.store(false);
|
||||
|
||||
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
|
||||
m_watcher = watcher;
|
||||
|
||||
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
|
||||
auto results = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (m_watcher == watcher)
|
||||
m_watcher = nullptr;
|
||||
emit finished(results);
|
||||
});
|
||||
|
||||
watcher->setFuture(QtConcurrent::run([this, provider, req]() {
|
||||
return runScan(provider, req);
|
||||
}));
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
const ScanRequest& req)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QVector<ScanResult> results;
|
||||
|
||||
if (!prov || req.pattern.isEmpty())
|
||||
return results;
|
||||
|
||||
auto regions = prov->enumerateRegions();
|
||||
qDebug() << "[scan] regions:" << regions.size()
|
||||
<< " pattern:" << req.pattern.size() << "bytes"
|
||||
<< " align:" << req.alignment
|
||||
<< " filterExec:" << req.filterExecutable
|
||||
<< " filterWrite:" << req.filterWritable;
|
||||
|
||||
// Fallback for providers that don't enumerate regions (file/buffer)
|
||||
if (regions.isEmpty()) {
|
||||
MemoryRegion fallback;
|
||||
fallback.base = 0;
|
||||
fallback.size = (uint64_t)prov->size();
|
||||
fallback.readable = true;
|
||||
fallback.writable = true;
|
||||
fallback.executable = false;
|
||||
regions.append(fallback);
|
||||
}
|
||||
|
||||
const int patternLen = req.pattern.size();
|
||||
const char* pat = req.pattern.constData();
|
||||
const char* msk = req.mask.constData();
|
||||
const int alignment = qMax(1, req.alignment);
|
||||
|
||||
// Pre-compute total bytes for progress
|
||||
uint64_t totalBytes = 0;
|
||||
for (const auto& r : regions) {
|
||||
if (req.filterExecutable && !r.executable) continue;
|
||||
if (req.filterWritable && !r.writable) continue;
|
||||
totalBytes += r.size;
|
||||
}
|
||||
|
||||
qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";
|
||||
|
||||
if (totalBytes == 0) return results;
|
||||
|
||||
uint64_t scannedBytes = 0;
|
||||
int lastPct = -1;
|
||||
|
||||
constexpr int kChunk = 256 * 1024;
|
||||
|
||||
for (const auto& region : regions) {
|
||||
if (m_abort.load()) break;
|
||||
|
||||
if (req.filterExecutable && !region.executable) continue;
|
||||
if (req.filterWritable && !region.writable) continue;
|
||||
|
||||
if ((uint64_t)patternLen > region.size) {
|
||||
scannedBytes += region.size;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int overlap = patternLen - 1;
|
||||
QByteArray chunk(qMin((uint64_t)kChunk, region.size), Qt::Uninitialized);
|
||||
|
||||
for (uint64_t off = 0; off < region.size; ) {
|
||||
if (m_abort.load()) break;
|
||||
|
||||
uint64_t remaining = region.size - off;
|
||||
int readLen = (int)qMin((uint64_t)chunk.size(), remaining);
|
||||
|
||||
if (!prov->read(region.base + off, chunk.data(), readLen)) {
|
||||
// Skip unreadable chunk
|
||||
off += readLen;
|
||||
scannedBytes += readLen;
|
||||
continue;
|
||||
}
|
||||
|
||||
int scanEnd = readLen - patternLen;
|
||||
const char* data = chunk.constData();
|
||||
|
||||
for (int i = 0; i <= scanEnd; i += alignment) {
|
||||
bool match = true;
|
||||
for (int j = 0; j < patternLen; j++) {
|
||||
if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
ScanResult r;
|
||||
r.address = region.base + off + (uint64_t)i;
|
||||
r.regionModule = region.moduleName;
|
||||
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
|
||||
results.append(r);
|
||||
|
||||
if (results.size() >= req.maxResults)
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance with overlap to catch patterns that straddle chunks
|
||||
uint64_t advance;
|
||||
if (readLen > overlap)
|
||||
advance = (uint64_t)(readLen - overlap);
|
||||
else
|
||||
advance = 1; // prevent infinite loop on tiny regions
|
||||
scannedBytes += advance;
|
||||
off += advance;
|
||||
|
||||
// Throttled progress
|
||||
int pct = (int)(scannedBytes * 100 / totalBytes);
|
||||
if (pct > 100) pct = 100;
|
||||
if (pct != lastPct) {
|
||||
lastPct = pct;
|
||||
QMetaObject::invokeMethod(this, "progress",
|
||||
Qt::QueuedConnection, Q_ARG(int, pct));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
qDebug() << "[scan] done:" << results.size() << "results in" << timer.elapsed() << "ms"
|
||||
<< " scanned:" << (scannedBytes / 1024) << "KB";
|
||||
return results;
|
||||
}
|
||||
|
||||
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask) {
|
||||
if (isRunning()) return;
|
||||
|
||||
m_abort.store(false);
|
||||
|
||||
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
|
||||
m_watcher = watcher;
|
||||
|
||||
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
|
||||
auto results = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (m_watcher == watcher)
|
||||
m_watcher = nullptr;
|
||||
emit rescanFinished(results);
|
||||
});
|
||||
|
||||
watcher->setFuture(QtConcurrent::run(
|
||||
[this, provider, results = std::move(results), readSize,
|
||||
filterPattern, filterMask]() mutable {
|
||||
return runRescan(provider, std::move(results), readSize,
|
||||
filterPattern, filterMask);
|
||||
}));
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask) {
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
int total = results.size();
|
||||
if (total == 0 || !prov) return results;
|
||||
|
||||
bool hasFilter = !filterPattern.isEmpty();
|
||||
qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize
|
||||
<< "filter:" << (hasFilter ? "yes" : "no");
|
||||
|
||||
// Save previous values
|
||||
for (auto& r : results)
|
||||
r.previousValue = r.scanValue;
|
||||
|
||||
// Sort indices by address for sequential chunked reads
|
||||
QVector<int> order(total);
|
||||
for (int i = 0; i < total; i++) order[i] = i;
|
||||
std::sort(order.begin(), order.end(), [&results](int a, int b) {
|
||||
return results[a].address < results[b].address;
|
||||
});
|
||||
|
||||
constexpr int kChunk = 256 * 1024;
|
||||
int updated = 0;
|
||||
int lastPct = -1;
|
||||
int chunks = 0;
|
||||
uint64_t totalBytesRead = 0;
|
||||
int i = 0;
|
||||
|
||||
// Track which results matched the filter (by original index)
|
||||
QVector<bool> matched(total, !hasFilter); // if no filter, all match
|
||||
|
||||
while (i < total && !m_abort.load()) {
|
||||
uint64_t spanBase = results[order[i]].address;
|
||||
int spanEnd = i;
|
||||
|
||||
// Extend span while next result fits in the same chunk
|
||||
while (spanEnd + 1 < total) {
|
||||
uint64_t endAddr = results[order[spanEnd + 1]].address + readSize;
|
||||
if (endAddr - spanBase > (uint64_t)kChunk) break;
|
||||
spanEnd++;
|
||||
}
|
||||
|
||||
uint64_t spanLast = results[order[spanEnd]].address;
|
||||
int chunkLen = (int)(spanLast + readSize - spanBase);
|
||||
QByteArray chunk(chunkLen, '\0');
|
||||
prov->read(spanBase, chunk.data(), chunkLen);
|
||||
|
||||
for (int j = i; j <= spanEnd; j++) {
|
||||
int idx = order[j];
|
||||
auto& r = results[idx];
|
||||
int off = (int)(r.address - spanBase);
|
||||
r.scanValue = chunk.mid(off, readSize);
|
||||
|
||||
// Apply filter: compare re-read bytes against the new pattern
|
||||
if (hasFilter) {
|
||||
int patLen = filterPattern.size();
|
||||
if (r.scanValue.size() >= patLen) {
|
||||
bool ok = true;
|
||||
const char* data = r.scanValue.constData();
|
||||
const char* pat = filterPattern.constData();
|
||||
const char* msk = filterMask.constData();
|
||||
for (int k = 0; k < patLen; k++) {
|
||||
if ((data[k] & msk[k]) != (pat[k] & msk[k])) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
matched[idx] = ok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chunks++;
|
||||
totalBytesRead += chunkLen;
|
||||
updated += (spanEnd - i + 1);
|
||||
i = spanEnd + 1;
|
||||
|
||||
int pct = updated * 100 / total;
|
||||
if (pct != lastPct) {
|
||||
lastPct = pct;
|
||||
QMetaObject::invokeMethod(this, "progress",
|
||||
Qt::QueuedConnection, Q_ARG(int, pct));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out non-matching results
|
||||
if (hasFilter) {
|
||||
QVector<ScanResult> filtered;
|
||||
filtered.reserve(total);
|
||||
for (int k = 0; k < total; k++) {
|
||||
if (matched[k])
|
||||
filtered.append(std::move(results[k]));
|
||||
}
|
||||
qDebug() << "[rescan] done:" << filtered.size() << "/" << total
|
||||
<< "matched in" << timer.elapsed() << "ms |" << chunks
|
||||
<< "chunks," << (totalBytesRead / 1024) << "KB read";
|
||||
return filtered;
|
||||
}
|
||||
|
||||
qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
|
||||
<< timer.elapsed() << "ms |" << chunks << "chunks,"
|
||||
<< (totalBytesRead / 1024) << "KB read";
|
||||
return results;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
94
src/scanner.h
Normal file
94
src/scanner.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
#include "providers/provider.h"
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QFutureWatcher>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Scan request / result ──
|
||||
|
||||
struct ScanRequest {
|
||||
QByteArray pattern; // literal bytes to match
|
||||
QByteArray mask; // 0xFF = must match, 0x00 = wildcard
|
||||
|
||||
bool filterExecutable = false; // only scan +x regions
|
||||
bool filterWritable = false; // only scan +w regions
|
||||
|
||||
int alignment = 1; // 1 = every byte, 4 = dword, 8 = qword
|
||||
int maxResults = 50000;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
uint64_t address;
|
||||
QString regionModule;
|
||||
QByteArray scanValue; // cached bytes at scan/update time
|
||||
QByteArray previousValue; // value before last update
|
||||
};
|
||||
|
||||
// ── Value scan types ──
|
||||
|
||||
enum class ValueType {
|
||||
Int8, Int16, Int32, Int64,
|
||||
UInt8, UInt16, UInt32, UInt64,
|
||||
Float, Double,
|
||||
Vec2, Vec3, Vec4,
|
||||
UTF8, UTF16,
|
||||
HexBytes
|
||||
};
|
||||
|
||||
// ── Pattern parsing ──
|
||||
|
||||
// Parse IDA-style signature string ("48 8B ?? 05") into pattern + mask.
|
||||
// Returns true on success. On failure, sets errorMsg.
|
||||
bool parseSignature(const QString& input, QByteArray& pattern, QByteArray& mask,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Serialize a typed value into raw bytes for exact-match scanning.
|
||||
// Returns true on success. On failure, sets errorMsg.
|
||||
bool serializeValue(ValueType type, const QString& input,
|
||||
QByteArray& pattern, QByteArray& mask,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Natural alignment for a value type (used as default alignment for value scans).
|
||||
int naturalAlignment(ValueType type);
|
||||
|
||||
// ── Scan engine ──
|
||||
|
||||
class ScanEngine : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ScanEngine(QObject* parent = nullptr);
|
||||
|
||||
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
||||
void startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern = {},
|
||||
const QByteArray& filterMask = {});
|
||||
void abort();
|
||||
bool isRunning() const;
|
||||
|
||||
signals:
|
||||
void progress(int percent);
|
||||
void finished(QVector<ScanResult> results);
|
||||
void rescanFinished(QVector<ScanResult> results);
|
||||
void error(QString message);
|
||||
|
||||
private:
|
||||
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
||||
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize,
|
||||
const QByteArray& filterPattern,
|
||||
const QByteArray& filterMask);
|
||||
|
||||
std::atomic<bool> m_abort{false};
|
||||
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
Q_DECLARE_METATYPE(QVector<rcx::ScanResult>)
|
||||
745
src/scannerpanel.cpp
Normal file
745
src/scannerpanel.cpp
Normal file
@@ -0,0 +1,745 @@
|
||||
#include "scannerpanel.h"
|
||||
#include "addressparser.h"
|
||||
#include <cstring>
|
||||
#include <QElapsedTimer>
|
||||
#include <QDebug>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
void AddressDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const {
|
||||
// Draw background (selection/hover handled by style)
|
||||
QStyleOptionViewItem opt = option;
|
||||
initStyleOption(&opt, index);
|
||||
opt.text.clear();
|
||||
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
|
||||
|
||||
QString text = index.data(Qt::DisplayRole).toString();
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
// Find first non-zero hex digit (skip backtick)
|
||||
int dimEnd = 0;
|
||||
for (int i = 0; i < text.size(); i++) {
|
||||
QChar c = text[i];
|
||||
if (c == '`') { dimEnd = i + 1; continue; }
|
||||
if (c != '0') break;
|
||||
dimEnd = i + 1;
|
||||
}
|
||||
|
||||
QRect textRect = opt.rect.adjusted(7, 0, -4, 0); // match item padding
|
||||
painter->setFont(opt.font);
|
||||
|
||||
if (dimEnd > 0) {
|
||||
painter->setPen(dimColor);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text.left(dimEnd));
|
||||
// Advance past dim prefix
|
||||
int dimWidth = painter->fontMetrics().horizontalAdvance(text.left(dimEnd));
|
||||
textRect.setLeft(textRect.left() + dimWidth);
|
||||
}
|
||||
painter->setPen(brightColor);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text.mid(dimEnd));
|
||||
}
|
||||
|
||||
ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_engine(new ScanEngine(this))
|
||||
{
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(6, 6, 6, 6);
|
||||
mainLayout->setSpacing(4);
|
||||
|
||||
// ── Row 1: Mode + pattern/value input ──
|
||||
auto* inputRow = new QHBoxLayout;
|
||||
inputRow->setSpacing(6);
|
||||
|
||||
m_modeCombo = new QComboBox(this);
|
||||
m_modeCombo->addItem(QIcon(QStringLiteral(":/vsicons/regex.svg")),
|
||||
QStringLiteral("Signature"));
|
||||
m_modeCombo->addItem(QIcon(QStringLiteral(":/vsicons/symbol-variable.svg")),
|
||||
QStringLiteral("Value"));
|
||||
updateComboWidth();
|
||||
inputRow->addWidget(m_modeCombo);
|
||||
|
||||
// Signature input
|
||||
m_patternLabel = new QLabel(QStringLiteral("Pattern:"), this);
|
||||
inputRow->addWidget(m_patternLabel);
|
||||
|
||||
m_patternEdit = new QLineEdit(this);
|
||||
m_patternEdit->setPlaceholderText(QStringLiteral("48 8B ?? 05 ?? ?? ?? ?? CC"));
|
||||
inputRow->addWidget(m_patternEdit, 1);
|
||||
|
||||
// Value input (hidden initially)
|
||||
m_typeLabel = new QLabel(QStringLiteral("Type:"), this);
|
||||
inputRow->addWidget(m_typeLabel);
|
||||
|
||||
m_typeCombo = new QComboBox(this);
|
||||
m_typeCombo->addItem(QStringLiteral("int8"), (int)ValueType::Int8);
|
||||
m_typeCombo->addItem(QStringLiteral("int16"), (int)ValueType::Int16);
|
||||
m_typeCombo->addItem(QStringLiteral("int32"), (int)ValueType::Int32);
|
||||
m_typeCombo->addItem(QStringLiteral("int64"), (int)ValueType::Int64);
|
||||
m_typeCombo->addItem(QStringLiteral("uint8"), (int)ValueType::UInt8);
|
||||
m_typeCombo->addItem(QStringLiteral("uint16"), (int)ValueType::UInt16);
|
||||
m_typeCombo->addItem(QStringLiteral("uint32"), (int)ValueType::UInt32);
|
||||
m_typeCombo->addItem(QStringLiteral("uint64"), (int)ValueType::UInt64);
|
||||
m_typeCombo->addItem(QStringLiteral("float"), (int)ValueType::Float);
|
||||
m_typeCombo->addItem(QStringLiteral("double"), (int)ValueType::Double);
|
||||
m_typeCombo->setCurrentIndex(2); // default: int32
|
||||
inputRow->addWidget(m_typeCombo);
|
||||
|
||||
m_valueLabel = new QLabel(QStringLiteral("Value:"), this);
|
||||
inputRow->addWidget(m_valueLabel);
|
||||
|
||||
m_valueEdit = new QLineEdit(this);
|
||||
m_valueEdit->setPlaceholderText(QStringLiteral("12345"));
|
||||
inputRow->addWidget(m_valueEdit, 1);
|
||||
|
||||
mainLayout->addLayout(inputRow);
|
||||
|
||||
// ── Row 2: Filters + scan button + progress ──
|
||||
auto* filterRow = new QHBoxLayout;
|
||||
filterRow->setSpacing(6);
|
||||
|
||||
m_execCheck = new QCheckBox(QStringLiteral("Executable"), this);
|
||||
filterRow->addWidget(m_execCheck);
|
||||
|
||||
m_writeCheck = new QCheckBox(QStringLiteral("Writable"), this);
|
||||
filterRow->addWidget(m_writeCheck);
|
||||
|
||||
filterRow->addStretch();
|
||||
|
||||
m_scanBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/search.svg")),
|
||||
QStringLiteral("Scan"), this);
|
||||
filterRow->addWidget(m_scanBtn);
|
||||
|
||||
m_updateBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/refresh.svg")),
|
||||
QStringLiteral("Re-scan"), this);
|
||||
m_updateBtn->setEnabled(false);
|
||||
filterRow->addWidget(m_updateBtn);
|
||||
|
||||
m_progressBar = new QProgressBar(this);
|
||||
m_progressBar->setRange(0, 100);
|
||||
m_progressBar->setTextVisible(true);
|
||||
m_progressBar->setFixedWidth(150);
|
||||
m_progressBar->hide();
|
||||
filterRow->addWidget(m_progressBar);
|
||||
|
||||
mainLayout->addLayout(filterRow);
|
||||
|
||||
// ── Results table ──
|
||||
m_resultTable = new QTableWidget(this);
|
||||
m_resultTable->setColumnCount(2);
|
||||
m_resultTable->horizontalHeader()->hide();
|
||||
m_resultTable->verticalHeader()->hide();
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(true);
|
||||
m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_resultTable->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_resultTable->setEditTriggers(QAbstractItemView::DoubleClicked);
|
||||
m_resultTable->setShowGrid(false);
|
||||
m_resultTable->setMouseTracking(true);
|
||||
m_resultTable->setFocusPolicy(Qt::StrongFocus);
|
||||
m_resultTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
// Address column delegate for dimmed leading zeros
|
||||
m_addrDelegate = new AddressDelegate(this);
|
||||
m_resultTable->setItemDelegateForColumn(0, m_addrDelegate);
|
||||
mainLayout->addWidget(m_resultTable, 1);
|
||||
|
||||
// ── Row 3: Status + action buttons ──
|
||||
auto* actionRow = new QHBoxLayout;
|
||||
actionRow->setSpacing(6);
|
||||
|
||||
m_statusLabel = new QLabel(QStringLiteral("Ready"), this);
|
||||
actionRow->addWidget(m_statusLabel, 1);
|
||||
|
||||
m_gotoBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/arrow-right.svg")),
|
||||
QStringLiteral("Go to Address"), this);
|
||||
m_gotoBtn->setEnabled(false);
|
||||
actionRow->addWidget(m_gotoBtn);
|
||||
|
||||
m_copyBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
|
||||
QStringLiteral("Copy Address"), this);
|
||||
m_copyBtn->setEnabled(false);
|
||||
actionRow->addWidget(m_copyBtn);
|
||||
|
||||
mainLayout->addLayout(actionRow);
|
||||
|
||||
// ── Initial state: signature mode ──
|
||||
m_typeLabel->hide();
|
||||
m_typeCombo->hide();
|
||||
m_valueLabel->hide();
|
||||
m_valueEdit->hide();
|
||||
m_execCheck->setChecked(true);
|
||||
|
||||
// ── Connections ──
|
||||
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ScannerPanel::onModeChanged);
|
||||
connect(m_scanBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onScanClicked);
|
||||
connect(m_updateBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onUpdateClicked);
|
||||
connect(m_gotoBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onGoToAddress);
|
||||
connect(m_copyBtn, &QPushButton::clicked,
|
||||
this, &ScannerPanel::onCopyAddress);
|
||||
connect(m_resultTable, &QTableWidget::cellDoubleClicked,
|
||||
this, &ScannerPanel::onResultDoubleClicked);
|
||||
connect(m_resultTable, &QTableWidget::cellChanged,
|
||||
this, &ScannerPanel::onCellEdited);
|
||||
connect(m_resultTable, &QTableWidget::itemSelectionChanged, this, [this]() {
|
||||
bool hasSel = !m_resultTable->selectedItems().isEmpty();
|
||||
m_gotoBtn->setEnabled(hasSel);
|
||||
m_copyBtn->setEnabled(hasSel);
|
||||
});
|
||||
|
||||
connect(m_resultTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
int row = m_resultTable->rowAt(pos.y());
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
QMenu menu;
|
||||
auto* copyAddr = menu.addAction(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
|
||||
QStringLiteral("Copy Address"));
|
||||
auto* copyVal = menu.addAction(QIcon(QStringLiteral(":/vsicons/clippy.svg")),
|
||||
QStringLiteral("Copy Value"));
|
||||
auto* goTo = menu.addAction(QIcon(QStringLiteral(":/vsicons/arrow-right.svg")),
|
||||
QStringLiteral("Go to Address"));
|
||||
auto* chosen = menu.exec(m_resultTable->viewport()->mapToGlobal(pos));
|
||||
if (chosen == copyAddr) {
|
||||
QString addr = QStringLiteral("0x%1")
|
||||
.arg(m_results[row].address, 0, 16, QLatin1Char('0')).toUpper();
|
||||
QApplication::clipboard()->setText(addr);
|
||||
m_statusLabel->setText(QStringLiteral("Copied: %1").arg(addr));
|
||||
} else if (chosen == copyVal) {
|
||||
QApplication::clipboard()->setText(formatValue(m_results[row].scanValue));
|
||||
m_statusLabel->setText(QStringLiteral("Copied value"));
|
||||
} else if (chosen == goTo) {
|
||||
emit goToAddress(m_results[row].address);
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_engine, &ScanEngine::progress, this, [this](int pct) {
|
||||
m_progressBar->setValue(pct);
|
||||
});
|
||||
connect(m_engine, &ScanEngine::finished,
|
||||
this, &ScannerPanel::onScanFinished);
|
||||
connect(m_engine, &ScanEngine::rescanFinished,
|
||||
this, &ScannerPanel::onRescanFinished);
|
||||
connect(m_engine, &ScanEngine::error, this, [this](const QString& msg) {
|
||||
m_statusLabel->setText(QStringLiteral("Error: %1").arg(msg));
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
});
|
||||
}
|
||||
|
||||
void ScannerPanel::setProviderGetter(ProviderGetter getter) {
|
||||
m_providerGetter = std::move(getter);
|
||||
}
|
||||
|
||||
void ScannerPanel::setEditorFont(const QFont& font) {
|
||||
m_resultTable->setFont(font);
|
||||
QFontMetrics fm(font);
|
||||
m_resultTable->verticalHeader()->setDefaultSectionSize(fm.height() + 6);
|
||||
// Address column width: "00000000`00000000" + padding
|
||||
m_resultTable->setColumnWidth(0, fm.horizontalAdvance(QStringLiteral("00000000`00000000")) + 20);
|
||||
m_patternEdit->setFont(font);
|
||||
m_valueEdit->setFont(font);
|
||||
m_modeCombo->setFont(font);
|
||||
m_typeCombo->setFont(font);
|
||||
m_statusLabel->setFont(font);
|
||||
m_scanBtn->setFont(font);
|
||||
m_gotoBtn->setFont(font);
|
||||
m_copyBtn->setFont(font);
|
||||
m_patternLabel->setFont(font);
|
||||
m_typeLabel->setFont(font);
|
||||
m_valueLabel->setFont(font);
|
||||
m_execCheck->setFont(font);
|
||||
m_writeCheck->setFont(font);
|
||||
m_updateBtn->setFont(font);
|
||||
updateComboWidth();
|
||||
}
|
||||
|
||||
void ScannerPanel::updateComboWidth() {
|
||||
QFontMetrics fm(m_modeCombo->font());
|
||||
int maxW = 0;
|
||||
for (int i = 0; i < m_modeCombo->count(); i++)
|
||||
maxW = qMax(maxW, fm.horizontalAdvance(m_modeCombo->itemText(i)));
|
||||
m_modeCombo->setFixedWidth(maxW + 50); // icon + dropdown arrow + padding
|
||||
}
|
||||
|
||||
void ScannerPanel::onModeChanged(int index) {
|
||||
bool isSig = (index == 0);
|
||||
|
||||
m_patternLabel->setVisible(isSig);
|
||||
m_patternEdit->setVisible(isSig);
|
||||
|
||||
m_typeLabel->setVisible(!isSig);
|
||||
m_typeCombo->setVisible(!isSig);
|
||||
m_valueLabel->setVisible(!isSig);
|
||||
m_valueEdit->setVisible(!isSig);
|
||||
|
||||
// Auto-toggle filters: signatures → executable code, values → writable data
|
||||
m_execCheck->setChecked(isSig);
|
||||
m_writeCheck->setChecked(!isSig);
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanClicked() {
|
||||
if (m_engine->isRunning()) {
|
||||
m_engine->abort();
|
||||
return; // finished/rescanFinished handler resets UI
|
||||
}
|
||||
|
||||
// Get provider
|
||||
std::shared_ptr<Provider> provider;
|
||||
if (m_providerGetter)
|
||||
provider = m_providerGetter();
|
||||
|
||||
if (!provider) {
|
||||
m_statusLabel->setText(QStringLiteral("No source attached"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build request
|
||||
ScanRequest req = buildRequest();
|
||||
if (req.pattern.isEmpty())
|
||||
return; // error already shown by buildRequest
|
||||
|
||||
m_lastScanMode = m_modeCombo->currentIndex();
|
||||
if (m_lastScanMode == 1)
|
||||
m_lastValueType = (ValueType)m_typeCombo->currentData().toInt();
|
||||
m_lastPattern = req.pattern;
|
||||
|
||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
m_statusLabel->setText(QStringLiteral("Scanning..."));
|
||||
|
||||
m_engine->start(provider, req);
|
||||
}
|
||||
|
||||
ScanRequest ScannerPanel::buildRequest() {
|
||||
ScanRequest req;
|
||||
QString err;
|
||||
|
||||
if (m_modeCombo->currentIndex() == 0) {
|
||||
// Signature mode
|
||||
if (!parseSignature(m_patternEdit->text(), req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return {};
|
||||
}
|
||||
req.alignment = 1;
|
||||
} else {
|
||||
// Value mode
|
||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||
if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return {};
|
||||
}
|
||||
req.alignment = naturalAlignment(vt);
|
||||
}
|
||||
|
||||
req.filterExecutable = m_execCheck->isChecked();
|
||||
req.filterWritable = m_writeCheck->isChecked();
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_results = std::move(results);
|
||||
|
||||
// Bytes are cached by the engine during scan.
|
||||
// Value mode: override with exact search pattern (engine caches raw chunk bytes).
|
||||
for (auto& r : m_results) {
|
||||
r.previousValue.clear();
|
||||
if (m_lastScanMode == 1)
|
||||
r.scanValue = m_lastPattern;
|
||||
}
|
||||
|
||||
m_updateBtn->setEnabled(!m_results.isEmpty());
|
||||
{
|
||||
QElapsedTimer pt;
|
||||
pt.start();
|
||||
populateTable(false);
|
||||
qDebug() << "[panel] populateTable(initial):" << m_results.size()
|
||||
<< "results," << pt.elapsed() << "ms";
|
||||
}
|
||||
|
||||
int n = m_results.size();
|
||||
m_statusLabel->setText(QStringLiteral("%1 result%2")
|
||||
.arg(n).arg(n == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::populateTable(bool showPrevious) {
|
||||
constexpr int kMaxRows = 10000;
|
||||
|
||||
m_resultTable->blockSignals(true);
|
||||
int cols = showPrevious ? 3 : 2;
|
||||
m_resultTable->setColumnCount(cols);
|
||||
int displayCount = qMin(m_results.size(), kMaxRows);
|
||||
m_resultTable->setRowCount(displayCount);
|
||||
|
||||
for (int i = 0; i < displayCount; i++) {
|
||||
const auto& r = m_results[i];
|
||||
|
||||
// Address column — WinDbg backtick format: 00000000`00000000
|
||||
QString hexPart = QStringLiteral("%1").arg(r.address, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
auto* addrItem = new QTableWidgetItem(hexPart);
|
||||
addrItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 0, addrItem);
|
||||
|
||||
// Value column
|
||||
auto* valItem = new QTableWidgetItem(formatValue(r.scanValue));
|
||||
valItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 1, valItem);
|
||||
|
||||
// Previous column
|
||||
if (showPrevious) {
|
||||
auto* prevItem = new QTableWidgetItem(
|
||||
r.previousValue.isEmpty() ? QString() : formatValue(r.previousValue));
|
||||
prevItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
m_resultTable->setItem(i, 2, prevItem);
|
||||
}
|
||||
}
|
||||
|
||||
m_resultTable->blockSignals(false);
|
||||
}
|
||||
|
||||
void ScannerPanel::onUpdateClicked() {
|
||||
if (m_results.isEmpty() || m_engine->isRunning()) return;
|
||||
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
if (!prov) {
|
||||
m_statusLabel->setText(QStringLiteral("No source attached"));
|
||||
return;
|
||||
}
|
||||
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
|
||||
// Build filter from current input field
|
||||
QByteArray filterPattern, filterMask;
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode
|
||||
QString err;
|
||||
if (!m_patternEdit->text().trimmed().isEmpty()) {
|
||||
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Value mode
|
||||
QString err;
|
||||
if (!m_valueEdit->text().trimmed().isEmpty()) {
|
||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
|
||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||
return;
|
||||
}
|
||||
m_lastValueType = vt;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last pattern so display uses the new value
|
||||
if (!filterPattern.isEmpty())
|
||||
m_lastPattern = filterPattern;
|
||||
|
||||
m_preRescanCount = m_results.size();
|
||||
m_updateBtn->setEnabled(false);
|
||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
|
||||
m_engine->startRescan(prov, m_results, readSize, filterPattern, filterMask);
|
||||
}
|
||||
|
||||
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_results = std::move(results);
|
||||
m_updateBtn->setEnabled(!m_results.isEmpty());
|
||||
|
||||
{
|
||||
QElapsedTimer pt;
|
||||
pt.start();
|
||||
populateTable(true);
|
||||
qDebug() << "[panel] populateTable(rescan):" << m_results.size()
|
||||
<< "results," << pt.elapsed() << "ms";
|
||||
}
|
||||
|
||||
int n = m_results.size();
|
||||
if (m_preRescanCount > 0 && n < m_preRescanCount)
|
||||
m_statusLabel->setText(QStringLiteral("%1 of %2 results match")
|
||||
.arg(n).arg(m_preRescanCount));
|
||||
else
|
||||
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
|
||||
.arg(n).arg(n == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::onGoToAddress() {
|
||||
int row = m_resultTable->currentRow();
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
emit goToAddress(m_results[row].address);
|
||||
}
|
||||
|
||||
void ScannerPanel::onCopyAddress() {
|
||||
int row = m_resultTable->currentRow();
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
|
||||
QString addr = QStringLiteral("0x%1")
|
||||
.arg(m_results[row].address, 0, 16, QLatin1Char('0')).toUpper();
|
||||
QApplication::clipboard()->setText(addr);
|
||||
m_statusLabel->setText(QStringLiteral("Copied: %1").arg(addr));
|
||||
}
|
||||
|
||||
void ScannerPanel::onResultDoubleClicked(int row, int col) {
|
||||
// Double-click on address column navigates (editing also starts via edit trigger)
|
||||
// Double-click on preview column only starts inline editing
|
||||
Q_UNUSED(col);
|
||||
Q_UNUSED(row);
|
||||
// Navigation is handled by Go to Address button or onCellEdited for address expressions
|
||||
}
|
||||
|
||||
void ScannerPanel::onCellEdited(int row, int col) {
|
||||
if (row < 0 || row >= m_results.size()) return;
|
||||
|
||||
auto* item = m_resultTable->item(row, col);
|
||||
if (!item) return;
|
||||
QString text = item->text().trimmed();
|
||||
|
||||
if (col == 0) {
|
||||
// Address column — evaluate expression via AddressParser
|
||||
AddressParserCallbacks cbs;
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
if (prov) {
|
||||
auto* p = prov.get();
|
||||
cbs.resolveModule = [p](const QString& name, bool* ok) -> uint64_t {
|
||||
uint64_t base = p->symbolToAddress(name);
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
int ptrSz = p->pointerSize();
|
||||
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = p->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
}
|
||||
int evalPtrSize = prov ? prov->pointerSize() : 8;
|
||||
auto result = AddressParser::evaluate(text, evalPtrSize, &cbs);
|
||||
if (result.ok) {
|
||||
m_results[row].address = result.value;
|
||||
emit goToAddress(result.value);
|
||||
// Reformat the address cell
|
||||
m_resultTable->blockSignals(true);
|
||||
QString hexPart = QStringLiteral("%1").arg(result.value, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
item->setText(hexPart);
|
||||
// Re-read preview at new address and update cache
|
||||
if (prov) {
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
m_results[row].scanValue = prov->readBytes(result.value, readSize);
|
||||
if (auto* prevItem = m_resultTable->item(row, 1))
|
||||
prevItem->setText(formatValue(m_results[row].scanValue));
|
||||
}
|
||||
m_resultTable->blockSignals(false);
|
||||
} else {
|
||||
m_statusLabel->setText(QStringLiteral("Expression error: %1").arg(result.error));
|
||||
// Restore original address
|
||||
m_resultTable->blockSignals(true);
|
||||
QString hexPart = QStringLiteral("%1").arg(m_results[row].address, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
item->setText(hexPart);
|
||||
m_resultTable->blockSignals(false);
|
||||
}
|
||||
} else if (col == 1) {
|
||||
// Preview column — parse hex bytes and write to provider
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
if (!prov || !prov->isWritable()) {
|
||||
m_statusLabel->setText(QStringLiteral("Provider is read-only"));
|
||||
return;
|
||||
}
|
||||
QByteArray bytes;
|
||||
uint64_t addr = m_results[row].address;
|
||||
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode — parse space-separated hex bytes
|
||||
QStringList tokens = text.split(' ', Qt::SkipEmptyParts);
|
||||
for (const QString& tok : tokens) {
|
||||
bool ok;
|
||||
uint val = tok.toUInt(&ok, 16);
|
||||
if (!ok || val > 0xFF) {
|
||||
m_statusLabel->setText(QStringLiteral("Invalid hex byte: %1").arg(tok));
|
||||
return;
|
||||
}
|
||||
bytes.append(char(val));
|
||||
}
|
||||
} else {
|
||||
// Value mode — parse native type
|
||||
bool ok = false;
|
||||
bytes.resize(valueSize());
|
||||
char* d = bytes.data();
|
||||
switch (m_lastValueType) {
|
||||
case ValueType::Int8: { auto v = (int8_t)text.toInt(&ok); if (ok) memcpy(d, &v, 1); break; }
|
||||
case ValueType::UInt8: { auto v = (uint8_t)text.toUInt(&ok); if (ok) memcpy(d, &v, 1); break; }
|
||||
case ValueType::Int16: { auto v = (int16_t)text.toShort(&ok); if (ok) memcpy(d, &v, 2); break; }
|
||||
case ValueType::UInt16: { auto v = text.toUShort(&ok); if (ok) memcpy(d, &v, 2); break; }
|
||||
case ValueType::Int32: { auto v = text.toInt(&ok); if (ok) memcpy(d, &v, 4); break; }
|
||||
case ValueType::UInt32: { auto v = text.toUInt(&ok); if (ok) memcpy(d, &v, 4); break; }
|
||||
case ValueType::Int64: { auto v = text.toLongLong(&ok); if (ok) memcpy(d, &v, 8); break; }
|
||||
case ValueType::UInt64: { auto v = text.toULongLong(&ok); if (ok) memcpy(d, &v, 8); break; }
|
||||
case ValueType::Float: { auto v = text.toFloat(&ok); if (ok) memcpy(d, &v, 4); break; }
|
||||
case ValueType::Double: { auto v = text.toDouble(&ok); if (ok) memcpy(d, &v, 8); break; }
|
||||
default: break;
|
||||
}
|
||||
if (!ok) {
|
||||
m_statusLabel->setText(QStringLiteral("Invalid value"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (bytes.isEmpty()) return;
|
||||
|
||||
if (prov->writeBytes(addr, bytes)) {
|
||||
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
|
||||
.arg(bytes.size())
|
||||
.arg(bytes.size() == 1 ? "" : "s")
|
||||
.arg(addr, 0, 16, QLatin1Char('0')).toUpper());
|
||||
// Re-read and update cache
|
||||
m_resultTable->blockSignals(true);
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
m_results[row].scanValue = prov->readBytes(addr, readSize);
|
||||
item->setText(formatValue(m_results[row].scanValue));
|
||||
m_resultTable->blockSignals(false);
|
||||
} else {
|
||||
m_statusLabel->setText(QStringLiteral("Write failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerPanel::applyTheme(const Theme& theme) {
|
||||
// Address delegate colors
|
||||
m_addrDelegate->dimColor = theme.textFaint;
|
||||
m_addrDelegate->brightColor = theme.text;
|
||||
|
||||
// Results table — editor-matching style
|
||||
m_resultTable->setStyleSheet(QStringLiteral(
|
||||
"QTableWidget { background: %1; color: %2; border: none; }"
|
||||
"QTableWidget::item { padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget QLineEdit { background: %1; color: %2; border: 1px solid %4;"
|
||||
" padding: 1px 4px; selection-background-color: %5; }")
|
||||
.arg(theme.background.name(), theme.text.name(), theme.hover.name(),
|
||||
theme.borderFocused.name(), theme.selection.name()));
|
||||
|
||||
// Input fields
|
||||
QString lineEditStyle = QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
|
||||
"QLineEdit:focus { border-color: %4; }")
|
||||
.arg(theme.background.name(), theme.text.name(),
|
||||
theme.border.name(), theme.borderFocused.name());
|
||||
m_patternEdit->setStyleSheet(lineEditStyle);
|
||||
m_valueEdit->setStyleSheet(lineEditStyle);
|
||||
|
||||
// Combo boxes
|
||||
QString comboStyle = QStringLiteral(
|
||||
"QComboBox { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px 2px 4px; }"
|
||||
"QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right;"
|
||||
" width: 16px; border-left: 1px solid %3; }"
|
||||
"QComboBox::down-arrow { image: url(:/vsicons/chevron-down.svg); width: 10px; height: 10px; }"
|
||||
"QComboBox QAbstractItemView { background: %1; color: %2; selection-background-color: %4; }")
|
||||
.arg(theme.background.name(), theme.text.name(),
|
||||
theme.border.name(), theme.hover.name());
|
||||
m_modeCombo->setStyleSheet(comboStyle);
|
||||
m_typeCombo->setStyleSheet(comboStyle);
|
||||
|
||||
// Labels
|
||||
QPalette lp;
|
||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_patternLabel->setPalette(lp);
|
||||
m_typeLabel->setPalette(lp);
|
||||
m_valueLabel->setPalette(lp);
|
||||
m_statusLabel->setPalette(lp);
|
||||
|
||||
// Checkboxes
|
||||
QPalette cp;
|
||||
cp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_execCheck->setPalette(cp);
|
||||
m_writeCheck->setPalette(cp);
|
||||
|
||||
// Buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
|
||||
"QPushButton:hover { background: %4; }"
|
||||
"QPushButton:pressed { background: %5; }"
|
||||
"QPushButton:disabled { color: %6; }")
|
||||
.arg(theme.button.name(), theme.text.name(), theme.border.name(),
|
||||
theme.hover.name(), theme.hover.darker(130).name(),
|
||||
theme.textMuted.name());
|
||||
m_scanBtn->setStyleSheet(btnStyle);
|
||||
m_updateBtn->setStyleSheet(btnStyle);
|
||||
m_gotoBtn->setStyleSheet(btnStyle);
|
||||
m_copyBtn->setStyleSheet(btnStyle);
|
||||
|
||||
// Progress bar
|
||||
m_progressBar->setStyleSheet(QStringLiteral(
|
||||
"QProgressBar { background: %1; border: 1px solid %2; text-align: center; color: %3; }"
|
||||
"QProgressBar::chunk { background: %4; }")
|
||||
.arg(theme.background.name(), theme.border.name(),
|
||||
theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
}
|
||||
|
||||
int ScannerPanel::valueSize() const {
|
||||
switch (m_lastValueType) {
|
||||
case ValueType::Int8: case ValueType::UInt8: return 1;
|
||||
case ValueType::Int16: case ValueType::UInt16: return 2;
|
||||
case ValueType::Int32: case ValueType::UInt32: case ValueType::Float: return 4;
|
||||
case ValueType::Int64: case ValueType::UInt64: case ValueType::Double: return 8;
|
||||
default: return 16;
|
||||
}
|
||||
}
|
||||
|
||||
QString ScannerPanel::formatValue(const QByteArray& bytes) const {
|
||||
if (m_lastScanMode == 0) {
|
||||
// Signature mode — hex bytes
|
||||
QString s;
|
||||
for (int j = 0; j < bytes.size(); j++) {
|
||||
if (j > 0) s += ' ';
|
||||
s += QStringLiteral("%1").arg((uint8_t)bytes[j], 2, 16, QLatin1Char('0')).toUpper();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// Value mode — native type
|
||||
const char* d = bytes.constData();
|
||||
int sz = bytes.size();
|
||||
switch (m_lastValueType) {
|
||||
case ValueType::Int8: if (sz >= 1) return QString::number((int8_t)d[0]); break;
|
||||
case ValueType::UInt8: if (sz >= 1) return QString::number((uint8_t)d[0]); break;
|
||||
case ValueType::Int16: if (sz >= 2) { int16_t v; memcpy(&v, d, 2); return QString::number(v); } break;
|
||||
case ValueType::UInt16: if (sz >= 2) { uint16_t v; memcpy(&v, d, 2); return QString::number(v); } break;
|
||||
case ValueType::Int32: if (sz >= 4) { int32_t v; memcpy(&v, d, 4); return QString::number(v); } break;
|
||||
case ValueType::UInt32: if (sz >= 4) { uint32_t v; memcpy(&v, d, 4); return QString::number(v); } break;
|
||||
case ValueType::Int64: if (sz >= 8) { int64_t v; memcpy(&v, d, 8); return QString::number(v); } break;
|
||||
case ValueType::UInt64: if (sz >= 8) { uint64_t v; memcpy(&v, d, 8); return QString::number(v); } break;
|
||||
case ValueType::Float: if (sz >= 4) { float v; memcpy(&v, d, 4); return QString::number(v, 'g', 9); } break;
|
||||
case ValueType::Double: if (sz >= 8) { double v; memcpy(&v, d, 8); return QString::number(v, 'g', 17); } break;
|
||||
default: break;
|
||||
}
|
||||
return QStringLiteral("??");
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
113
src/scannerpanel.h
Normal file
113
src/scannerpanel.h
Normal file
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
#include "scanner.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QWidget>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include <QProgressBar>
|
||||
#include <QTableWidget>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QLabel>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Delegate that paints address with dimmed high-bytes prefix
|
||||
class AddressDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
QColor dimColor;
|
||||
QColor brightColor;
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override;
|
||||
};
|
||||
|
||||
class ScannerPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ScannerPanel(QWidget* parent = nullptr);
|
||||
|
||||
using ProviderGetter = std::function<std::shared_ptr<Provider>()>;
|
||||
void setProviderGetter(ProviderGetter getter);
|
||||
|
||||
void setEditorFont(const QFont& font);
|
||||
void applyTheme(const Theme& theme);
|
||||
|
||||
// Test accessors
|
||||
QComboBox* modeCombo() const { return m_modeCombo; }
|
||||
QLineEdit* patternEdit() const { return m_patternEdit; }
|
||||
QComboBox* typeCombo() const { return m_typeCombo; }
|
||||
QLineEdit* valueEdit() const { return m_valueEdit; }
|
||||
QCheckBox* execCheck() const { return m_execCheck; }
|
||||
QCheckBox* writeCheck() const { return m_writeCheck; }
|
||||
QPushButton* scanButton() const { return m_scanBtn; }
|
||||
QPushButton* updateButton() const { return m_updateBtn; }
|
||||
QProgressBar* progressBar() const { return m_progressBar; }
|
||||
QTableWidget* resultsTable() const { return m_resultTable; }
|
||||
QLabel* statusLabel() const { return m_statusLabel; }
|
||||
QPushButton* gotoButton() const { return m_gotoBtn; }
|
||||
QPushButton* copyButton() const { return m_copyBtn; }
|
||||
ScanEngine* engine() const { return m_engine; }
|
||||
|
||||
signals:
|
||||
void goToAddress(uint64_t address);
|
||||
|
||||
private slots:
|
||||
void onModeChanged(int index);
|
||||
void onScanClicked();
|
||||
void onScanFinished(QVector<ScanResult> results);
|
||||
void onGoToAddress();
|
||||
void onCopyAddress();
|
||||
void onResultDoubleClicked(int row, int col);
|
||||
void onCellEdited(int row, int col);
|
||||
void onUpdateClicked();
|
||||
void onRescanFinished(QVector<ScanResult> results);
|
||||
|
||||
private:
|
||||
ScanRequest buildRequest();
|
||||
void populateTable(bool showPrevious);
|
||||
void updateComboWidth();
|
||||
|
||||
// Input widgets
|
||||
QComboBox* m_modeCombo; // Signature / Value
|
||||
QLineEdit* m_patternEdit; // Signature pattern input
|
||||
QComboBox* m_typeCombo; // Value type dropdown
|
||||
QLineEdit* m_valueEdit; // Value input
|
||||
QLabel* m_patternLabel;
|
||||
QLabel* m_typeLabel;
|
||||
QLabel* m_valueLabel;
|
||||
|
||||
// Filters
|
||||
QCheckBox* m_execCheck;
|
||||
QCheckBox* m_writeCheck;
|
||||
|
||||
// Actions
|
||||
QPushButton* m_scanBtn;
|
||||
QPushButton* m_updateBtn;
|
||||
QProgressBar* m_progressBar;
|
||||
|
||||
// Results
|
||||
QTableWidget* m_resultTable;
|
||||
AddressDelegate* m_addrDelegate;
|
||||
QLabel* m_statusLabel;
|
||||
QPushButton* m_gotoBtn;
|
||||
QPushButton* m_copyBtn;
|
||||
|
||||
// Engine
|
||||
ScanEngine* m_engine;
|
||||
ProviderGetter m_providerGetter;
|
||||
QVector<ScanResult> m_results;
|
||||
int m_lastScanMode = 0; // 0=signature, 1=value
|
||||
ValueType m_lastValueType = ValueType::Int32;
|
||||
QByteArray m_lastPattern; // serialized search value
|
||||
int m_preRescanCount = 0; // result count before last rescan
|
||||
|
||||
QString formatValue(const QByteArray& bytes) const;
|
||||
int valueSize() const;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#585858",
|
||||
"textFaint": "#505050",
|
||||
"hover": "#1e1e1e",
|
||||
"selected": "#1e1e1e",
|
||||
"hover": "#2a2a2a",
|
||||
"selected": "#2a2d2e",
|
||||
"selection": "#2b2b2b",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "theme.h"
|
||||
#include <QtGlobal>
|
||||
#include <type_traits>
|
||||
|
||||
namespace rcx {
|
||||
@@ -61,6 +62,15 @@ Theme Theme::fromJson(const QJsonObject& o) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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++) {
|
||||
@@ -29,7 +33,12 @@ ThemeManager::ThemeManager() {
|
||||
// ── Load built-in themes from JSON files next to the executable ──
|
||||
|
||||
QString ThemeManager::builtInDir() const {
|
||||
#ifdef Q_OS_MACOS
|
||||
// In a macOS .app bundle, resources live in Contents/Resources, not Contents/MacOS
|
||||
return QCoreApplication::applicationDirPath() + "/../Resources/themes";
|
||||
#else
|
||||
return QCoreApplication::applicationDirPath() + "/themes";
|
||||
#endif
|
||||
}
|
||||
|
||||
void ThemeManager::loadBuiltInThemes() {
|
||||
|
||||
@@ -74,16 +74,18 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
// App label
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.textDim.name()));
|
||||
.arg(theme.text.name()));
|
||||
|
||||
// Menu bar 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.text);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
}
|
||||
|
||||
// Chrome buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
@@ -110,7 +112,7 @@ void TitleBarWidget::setShowIcon(bool show) {
|
||||
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(m_theme.textDim.name()));
|
||||
.arg(m_theme.text.name()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ private:
|
||||
QToolButton* m_btnClose = nullptr;
|
||||
|
||||
Theme m_theme;
|
||||
bool m_titleCase = true;
|
||||
bool m_titleCase = false;
|
||||
|
||||
QToolButton* makeChromeButton(const QString& iconPath);
|
||||
void toggleMaximize();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
#include <QFont>
|
||||
#include <QVector>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <cstdint>
|
||||
#include "core.h"
|
||||
|
||||
@@ -26,13 +27,19 @@ enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget };
|
||||
|
||||
struct TypeEntry {
|
||||
enum Kind { Primitive, Composite, Section };
|
||||
enum Category { CatPrimitive, CatType, CatEnum };
|
||||
|
||||
Kind entryKind = Primitive;
|
||||
Category category = CatPrimitive;
|
||||
NodeKind primitiveKind = NodeKind::Hex8; // valid when entryKind==Primitive
|
||||
uint64_t structId = 0; // valid when entryKind==Composite
|
||||
QString displayName;
|
||||
QString classKeyword; // "struct", "class", "enum" (Composite only)
|
||||
bool enabled = true; // false = grayed out (visible but not selectable)
|
||||
int sizeBytes = 0; // size in bytes (for display)
|
||||
int alignment = 0; // natural alignment in bytes
|
||||
int fieldCount = 0; // child field count (composite only)
|
||||
QStringList fieldSummary; // first ~6 fields: "0x00: float x"
|
||||
};
|
||||
|
||||
// ── Parsed type spec (shared between popup filter and inline edit) ──
|
||||
@@ -40,6 +47,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
|
||||
};
|
||||
|
||||
@@ -57,15 +65,21 @@ public:
|
||||
void setMode(TypePopupMode mode);
|
||||
void applyTheme(const Theme& theme);
|
||||
void setCurrentNodeSize(int bytes);
|
||||
void setPointerSize(int bytes);
|
||||
void setModifier(int modId, int arrayCount = 0);
|
||||
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
|
||||
void popup(const QPoint& globalPos);
|
||||
|
||||
/// Show popup instantly with skeleton placeholders; call setTypes() to fill content.
|
||||
void popupLoading(const QPoint& globalPos);
|
||||
|
||||
/// Force native window creation to avoid cold-start delay.
|
||||
void warmUp();
|
||||
|
||||
signals:
|
||||
void typeSelected(const TypeEntry& entry, const QString& fullText);
|
||||
void createNewTypeRequested();
|
||||
void saveRequested();
|
||||
void dismissed();
|
||||
|
||||
protected:
|
||||
@@ -76,27 +90,35 @@ private:
|
||||
QLabel* m_titleLabel = nullptr;
|
||||
QToolButton* m_escLabel = nullptr;
|
||||
QToolButton* m_createBtn = nullptr;
|
||||
QToolButton* m_saveBtn = nullptr;
|
||||
QLineEdit* m_filterEdit = nullptr;
|
||||
QLabel* m_previewLabel = nullptr;
|
||||
QListView* m_listView = nullptr;
|
||||
QStringListModel* m_model = nullptr;
|
||||
QFrame* m_separator = nullptr;
|
||||
|
||||
// Modifier toggles
|
||||
QWidget* m_modRow = nullptr;
|
||||
QToolButton* m_btnPlain = nullptr;
|
||||
QToolButton* m_btnPtr = nullptr;
|
||||
QToolButton* m_btnDblPtr = nullptr;
|
||||
QToolButton* m_btnArray = nullptr;
|
||||
QLineEdit* m_arrayCountEdit = nullptr;
|
||||
QButtonGroup* m_modGroup = nullptr;
|
||||
|
||||
// Category filter checkboxes
|
||||
QWidget* m_chipRow = nullptr;
|
||||
QToolButton* m_chipPrim = nullptr;
|
||||
QToolButton* m_chipTypes = nullptr;
|
||||
QToolButton* m_chipEnums = nullptr;
|
||||
QLabel* m_statusLabel = nullptr;
|
||||
|
||||
QVector<TypeEntry> m_allTypes;
|
||||
QVector<TypeEntry> m_filteredTypes;
|
||||
QVector<QVector<int>> m_matchPositions;
|
||||
TypeEntry m_currentEntry;
|
||||
bool m_hasCurrent = false;
|
||||
TypePopupMode m_mode = TypePopupMode::FieldType;
|
||||
int m_currentNodeSize = 0;
|
||||
int m_pointerSize = 8;
|
||||
bool m_loading = false;
|
||||
QFont m_font;
|
||||
|
||||
void applyFilter(const QString& text);
|
||||
|
||||
@@ -29,46 +29,95 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
||||
|
||||
// Collect all top-level structs/enums across all tabs
|
||||
QVector<std::pair<const Node*, void*>> types, enums;
|
||||
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});
|
||||
enums.append({&n, tab.subPtr, tab.tree});
|
||||
else
|
||||
types.append({&n, tab.subPtr});
|
||||
types.append({&n, tab.subPtr, tab.tree});
|
||||
}
|
||||
}
|
||||
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
auto cmpName = [&](const std::pair<const Node*, void*>& a,
|
||||
const std::pair<const Node*, void*>& b) {
|
||||
return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0;
|
||||
// Sort structs by children count descending (most fields first)
|
||||
auto cmpChildren = [&](const Entry& a, const Entry& b) {
|
||||
int ca = a.tree->childrenOf(a.node->id).size();
|
||||
int cb = b.tree->childrenOf(b.node->id).size();
|
||||
if (ca != cb) return ca > cb;
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpChildren);
|
||||
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);
|
||||
|
||||
for (const auto& [n, subPtr] : types) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
// 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(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
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& [n, subPtr] : enums) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
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(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
224
tests/bench_large_class.cpp
Normal file
224
tests/bench_large_class.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* bench_large_class — benchmark compose, applyDocument, hover highlight,
|
||||
* and selection overlay on a large struct (500+ fields).
|
||||
*
|
||||
* Simulates EPROCESS-class structures to measure editor performance.
|
||||
*/
|
||||
#include <QtTest/QtTest>
|
||||
#include <QElapsedTimer>
|
||||
#include "core.h"
|
||||
#include "editor.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
/* ── Build a large struct tree with N fields of mixed types ──────── */
|
||||
|
||||
static NodeTree buildLargeTree(int fieldCount)
|
||||
{
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x7FF600000000ULL;
|
||||
|
||||
// Root struct
|
||||
Node root;
|
||||
root.id = 1;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("EPROCESS");
|
||||
root.structTypeName = QStringLiteral("_EPROCESS");
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
tree.addNode(root);
|
||||
|
||||
// Cycle through common field types
|
||||
const NodeKind kinds[] = {
|
||||
NodeKind::Int32, NodeKind::UInt64, NodeKind::Float,
|
||||
NodeKind::Pointer64, NodeKind::Int16, NodeKind::UInt32,
|
||||
NodeKind::Double, NodeKind::Bool, NodeKind::Hex8
|
||||
};
|
||||
const int kindCount = sizeof(kinds) / sizeof(kinds[0]);
|
||||
|
||||
int offset = 0;
|
||||
for (int i = 0; i < fieldCount; ++i) {
|
||||
Node n;
|
||||
n.id = (uint64_t)(i + 2);
|
||||
n.kind = kinds[i % kindCount];
|
||||
n.name = QStringLiteral("field_%1").arg(i, 4, 10, QChar('0'));
|
||||
n.parentId = 1;
|
||||
n.offset = offset;
|
||||
tree.addNode(n);
|
||||
offset += sizeForKind(n.kind);
|
||||
}
|
||||
tree.m_nextId = (uint64_t)(fieldCount + 2);
|
||||
return tree;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
class BenchLargeClass : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
NodeTree m_tree;
|
||||
BufferProvider m_prov;
|
||||
ComposeResult m_result;
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void benchCompose();
|
||||
void benchApplyDocument();
|
||||
void benchHoverHighlight();
|
||||
void benchSelectionOverlay();
|
||||
void benchHoverHighlightRepeated();
|
||||
|
||||
public:
|
||||
BenchLargeClass() : m_prov(QByteArray()) {}
|
||||
};
|
||||
|
||||
void BenchLargeClass::initTestCase()
|
||||
{
|
||||
m_tree = buildLargeTree(500);
|
||||
|
||||
// Create buffer large enough for all fields
|
||||
QByteArray buf(0x10000, '\0');
|
||||
// Fill with pattern so values are non-zero
|
||||
for (int i = 0; i < buf.size(); ++i)
|
||||
buf[i] = (char)(i & 0xFF);
|
||||
m_prov = BufferProvider(buf, QStringLiteral("bench_data"));
|
||||
|
||||
// Pre-compose for tests that need the result
|
||||
m_result = rcx::compose(m_tree, m_prov);
|
||||
qDebug() << "Tree:" << m_tree.nodes.size() << "nodes,"
|
||||
<< m_result.meta.size() << "display lines,"
|
||||
<< m_result.text.size() << "chars";
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchCompose()
|
||||
{
|
||||
const int ITERS = 100;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
ComposeResult r = rcx::compose(m_tree, m_prov);
|
||||
Q_UNUSED(r);
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Compose Benchmark (500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-compose:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchApplyDocument()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
|
||||
const int ITERS = 50;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
editor.applyDocument(m_result);
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== ApplyDocument Benchmark (500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-apply:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchHoverHighlight()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
editor.applyDocument(m_result);
|
||||
|
||||
// Simulate hovering over the first field
|
||||
// We need access to internals, so we measure via public methods
|
||||
// by toggling selection which triggers applyHoverHighlight internally
|
||||
QSet<uint64_t> sel;
|
||||
sel.insert(2); // first field node id
|
||||
|
||||
const int ITERS = 200;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
editor.applySelectionOverlay(i % 2 == 0 ? sel : QSet<uint64_t>{});
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Hover/Selection Overlay Benchmark (500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-cycle:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchSelectionOverlay()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
editor.applyDocument(m_result);
|
||||
|
||||
// Select many nodes (simulate multi-select of 50 fields)
|
||||
QSet<uint64_t> bigSel;
|
||||
for (int i = 0; i < 50; ++i)
|
||||
bigSel.insert((uint64_t)(i + 2));
|
||||
|
||||
const int ITERS = 100;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
editor.applySelectionOverlay(bigSel);
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Multi-Selection Overlay Benchmark (50 selected, 500 fields) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-overlay:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchHoverHighlightRepeated()
|
||||
{
|
||||
RcxEditor editor;
|
||||
editor.resize(800, 600);
|
||||
editor.applyDocument(m_result);
|
||||
|
||||
// Simulate rapid hover changes: alternate between two different nodes
|
||||
// This is the worst case - every call does a full marker clear + rescan
|
||||
QSet<uint64_t> empty;
|
||||
QSet<uint64_t> sel1; sel1.insert(10);
|
||||
QSet<uint64_t> sel2; sel2.insert(100);
|
||||
|
||||
const int ITERS = 500;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
editor.applySelectionOverlay(i % 3 == 0 ? sel1 : (i % 3 == 1 ? sel2 : empty));
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Rapid Hover Change Benchmark (500 fields, alternating nodes) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-change:" << (double)elapsed / ITERS << "ms";
|
||||
qDebug() << " Simulated events/sec:" << (ITERS * 1000.0 / elapsed);
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(BenchLargeClass)
|
||||
#include "bench_large_class.moc"
|
||||
440
tests/test_32bit_support.cpp
Normal file
440
tests/test_32bit_support.cpp
Normal file
@@ -0,0 +1,440 @@
|
||||
#include <QtTest/QTest>
|
||||
#include "core.h"
|
||||
#include "generator.h"
|
||||
#include "imports/import_source.h"
|
||||
#include "imports/import_reclass_xml.h"
|
||||
#include "providers/provider.h"
|
||||
#include "addressparser.h"
|
||||
#include "iplugin.h"
|
||||
#include "processpicker.h"
|
||||
|
||||
// Include RPC protocol for header size test
|
||||
#include "rcx_rpc_protocol.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// ── Test provider that reports a configurable pointer size ──
|
||||
|
||||
class TestProvider32 : public Provider {
|
||||
public:
|
||||
QByteArray m_data;
|
||||
int m_ptrSize;
|
||||
|
||||
TestProvider32(int ptrSize, int dataSize = 256)
|
||||
: m_ptrSize(ptrSize), m_data(dataSize, '\0') {}
|
||||
|
||||
bool read(uint64_t addr, void* buf, int len) const override {
|
||||
if ((int)addr + len > m_data.size()) {
|
||||
memset(buf, 0, len);
|
||||
return false;
|
||||
}
|
||||
memcpy(buf, m_data.constData() + addr, len);
|
||||
return true;
|
||||
}
|
||||
int size() const override { return m_data.size(); }
|
||||
int pointerSize() const override { return m_ptrSize; }
|
||||
};
|
||||
|
||||
class Test32BitSupport : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
|
||||
// ── 1. Provider::pointerSize() default is 8 ──
|
||||
|
||||
void providerDefaultPointerSize() {
|
||||
// NullProvider inherits default
|
||||
NullProvider np;
|
||||
QCOMPARE(np.pointerSize(), 8);
|
||||
}
|
||||
|
||||
void providerCustomPointerSize() {
|
||||
TestProvider32 p32(4);
|
||||
QCOMPARE(p32.pointerSize(), 4);
|
||||
TestProvider32 p64(8);
|
||||
QCOMPARE(p64.pointerSize(), 8);
|
||||
}
|
||||
|
||||
// ── 2. NodeTree pointerSize field ──
|
||||
|
||||
void nodeTreeDefaultPointerSize() {
|
||||
NodeTree tree;
|
||||
QCOMPARE(tree.pointerSize, 8);
|
||||
}
|
||||
|
||||
void nodeTreePointerSizeRoundTrip() {
|
||||
// 32-bit tree persists to JSON and back
|
||||
NodeTree tree;
|
||||
tree.pointerSize = 4;
|
||||
tree.baseAddress = 0x00400000;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
tree.addNode(root);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
QCOMPARE(json["pointerSize"].toInt(), 4);
|
||||
|
||||
NodeTree restored = NodeTree::fromJson(json);
|
||||
QCOMPARE(restored.pointerSize, 4);
|
||||
}
|
||||
|
||||
void nodeTreePointerSizeOmittedForDefault() {
|
||||
// 64-bit (default) should not write pointerSize key
|
||||
NodeTree tree;
|
||||
tree.pointerSize = 8;
|
||||
QJsonObject json = tree.toJson();
|
||||
QVERIFY(!json.contains("pointerSize"));
|
||||
}
|
||||
|
||||
void nodeTreePointerSizeDefaultOnMissing() {
|
||||
// Legacy JSON without pointerSize should default to 8
|
||||
QJsonObject json;
|
||||
json["baseAddress"] = "400000";
|
||||
json["nextId"] = "1";
|
||||
json["nodes"] = QJsonArray();
|
||||
|
||||
NodeTree tree = NodeTree::fromJson(json);
|
||||
QCOMPARE(tree.pointerSize, 8);
|
||||
}
|
||||
|
||||
// ── 3. Source import respects pointer size ──
|
||||
|
||||
void sourceImport64bitDefault() {
|
||||
QString src = R"(
|
||||
struct Test {
|
||||
PVOID ptr; // 0x0
|
||||
SIZE_T sz; // 0x8
|
||||
};
|
||||
)";
|
||||
QString error;
|
||||
NodeTree tree = importFromSource(src, &error);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||
|
||||
// Default: 64-bit pointers
|
||||
bool foundPtr64 = false, foundUInt64 = false;
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.name == "ptr" && n.kind == NodeKind::Pointer64) foundPtr64 = true;
|
||||
if (n.name == "sz" && n.kind == NodeKind::UInt64) foundUInt64 = true;
|
||||
}
|
||||
QVERIFY2(foundPtr64, "PVOID should be Pointer64 in 64-bit mode");
|
||||
QVERIFY2(foundUInt64, "SIZE_T should be UInt64 in 64-bit mode");
|
||||
}
|
||||
|
||||
void sourceImport32bit() {
|
||||
QString src = R"(
|
||||
struct Test {
|
||||
PVOID ptr; // 0x0
|
||||
SIZE_T sz; // 0x4
|
||||
};
|
||||
)";
|
||||
QString error;
|
||||
NodeTree tree = importFromSource(src, &error, 4);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||
|
||||
QCOMPARE(tree.pointerSize, 4);
|
||||
|
||||
bool foundPtr32 = false, foundUInt32 = false;
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.name == "ptr" && n.kind == NodeKind::Pointer32) foundPtr32 = true;
|
||||
if (n.name == "sz" && n.kind == NodeKind::UInt32) foundUInt32 = true;
|
||||
}
|
||||
QVERIFY2(foundPtr32, "PVOID should be Pointer32 in 32-bit mode");
|
||||
QVERIFY2(foundUInt32, "SIZE_T should be UInt32 in 32-bit mode");
|
||||
}
|
||||
|
||||
void sourceImportPointerField32bit() {
|
||||
// A generic pointer (void* field) should become Pointer32 in 32-bit mode
|
||||
QString src = R"(
|
||||
struct Test {
|
||||
void* ptr; // 0x0
|
||||
int value; // 0x4
|
||||
};
|
||||
)";
|
||||
QString error;
|
||||
NodeTree tree = importFromSource(src, &error, 4);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||
|
||||
bool foundPtr32 = false;
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.name == "ptr" && n.kind == NodeKind::Pointer32) foundPtr32 = true;
|
||||
}
|
||||
QVERIFY2(foundPtr32, "void* should be Pointer32 in 32-bit mode");
|
||||
}
|
||||
|
||||
void sourceImportPointerSizeTypes32bit() {
|
||||
// All pointer-size-dependent types should be 32-bit
|
||||
QString src = R"(
|
||||
struct Test {
|
||||
HANDLE h; // 0x0
|
||||
ULONG_PTR up; // 0x4
|
||||
LONG_PTR lp; // 0x8
|
||||
uintptr_t uip; // 0xC
|
||||
intptr_t ip; // 0x10
|
||||
size_t sz; // 0x14
|
||||
LPVOID lv; // 0x18
|
||||
PCHAR pc; // 0x1C
|
||||
};
|
||||
)";
|
||||
QString error;
|
||||
NodeTree tree = importFromSource(src, &error, 4);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.parentId == 0) continue; // skip root struct
|
||||
int sz = n.byteSize();
|
||||
QVERIFY2(sz == 4,
|
||||
qPrintable(QString("Field '%1' has size %2, expected 4")
|
||||
.arg(n.name).arg(sz)));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. Generator respects pointer size ──
|
||||
|
||||
void generatorPointer32NativeVoidStar() {
|
||||
// For 32-bit target, untyped Pointer32 should emit void*
|
||||
NodeTree tree;
|
||||
tree.pointerSize = 4;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node p;
|
||||
p.kind = NodeKind::Pointer32;
|
||||
p.name = "ptr";
|
||||
p.parentId = rootId;
|
||||
p.offset = 0;
|
||||
tree.addNode(p);
|
||||
|
||||
QString result = renderCpp(tree, rootId);
|
||||
QVERIFY2(result.contains("void* ptr;"),
|
||||
qPrintable("32-bit native Pointer32 should emit void*:\n" + result));
|
||||
}
|
||||
|
||||
void generatorPointer64NativeVoidStar() {
|
||||
// For 64-bit target (default), untyped Pointer64 should emit void*
|
||||
NodeTree tree;
|
||||
tree.pointerSize = 8;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node p;
|
||||
p.kind = NodeKind::Pointer64;
|
||||
p.name = "ptr";
|
||||
p.parentId = rootId;
|
||||
p.offset = 0;
|
||||
tree.addNode(p);
|
||||
|
||||
QString result = renderCpp(tree, rootId);
|
||||
QVERIFY2(result.contains("void* ptr;"),
|
||||
qPrintable("64-bit native Pointer64 should emit void*:\n" + result));
|
||||
}
|
||||
|
||||
void generatorPointer32CrossSizeInt() {
|
||||
// For 64-bit target, Pointer32 should emit uint32_t (cross-size)
|
||||
NodeTree tree;
|
||||
tree.pointerSize = 8;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node p;
|
||||
p.kind = NodeKind::Pointer32;
|
||||
p.name = "ptr32";
|
||||
p.parentId = rootId;
|
||||
p.offset = 0;
|
||||
tree.addNode(p);
|
||||
|
||||
QString result = renderCpp(tree, rootId);
|
||||
QVERIFY2(result.contains("uint32_t ptr32;"),
|
||||
qPrintable("Cross-size Pointer32 on 64-bit target should emit uint32_t:\n" + result));
|
||||
}
|
||||
|
||||
void generatorTypedPointerBothSizes() {
|
||||
// Typed pointers (with refId) always emit struct X* regardless of size
|
||||
NodeTree tree;
|
||||
tree.pointerSize = 4;
|
||||
|
||||
Node target;
|
||||
target.kind = NodeKind::Struct;
|
||||
target.name = "Target";
|
||||
target.structTypeName = "TargetData";
|
||||
target.parentId = 0;
|
||||
int ti = tree.addNode(target);
|
||||
uint64_t targetId = tree.nodes[ti].id;
|
||||
|
||||
Node main;
|
||||
main.kind = NodeKind::Struct;
|
||||
main.name = "Main";
|
||||
main.structTypeName = "MainStruct";
|
||||
main.parentId = 0;
|
||||
int mi = tree.addNode(main);
|
||||
uint64_t mainId = tree.nodes[mi].id;
|
||||
|
||||
Node p;
|
||||
p.kind = NodeKind::Pointer32;
|
||||
p.name = "pTarget";
|
||||
p.parentId = mainId;
|
||||
p.offset = 0;
|
||||
p.refId = targetId;
|
||||
tree.addNode(p);
|
||||
|
||||
QString result = renderCpp(tree, mainId);
|
||||
QVERIFY2(result.contains("struct TargetData* pTarget;"),
|
||||
qPrintable("Typed Pointer32 should emit struct X*:\n" + result));
|
||||
}
|
||||
|
||||
// ── 5. RPC protocol header has pointerSize field ──
|
||||
|
||||
void rpcHeaderHasPointerSize() {
|
||||
// Verify the field exists and header is still 4096 bytes
|
||||
RcxRpcHeader hdr = {};
|
||||
hdr.pointerSize = 4;
|
||||
QCOMPARE(hdr.pointerSize, (uint32_t)4);
|
||||
QCOMPARE((int)sizeof(RcxRpcHeader), RCX_RPC_HEADER_SIZE);
|
||||
}
|
||||
|
||||
// ── 6. PluginProcessInfo has is32Bit field ──
|
||||
|
||||
void pluginProcessInfoIs32Bit() {
|
||||
PluginProcessInfo info;
|
||||
QCOMPARE(info.is32Bit, false); // default
|
||||
|
||||
info.is32Bit = true;
|
||||
QCOMPARE(info.is32Bit, true);
|
||||
}
|
||||
|
||||
// ── 7. ProcessInfo has is32Bit field ──
|
||||
|
||||
void processInfoIs32Bit() {
|
||||
ProcessInfo info;
|
||||
QCOMPARE(info.is32Bit, false); // default
|
||||
|
||||
info.is32Bit = true;
|
||||
QCOMPARE(info.is32Bit, true);
|
||||
}
|
||||
|
||||
// ── 8. AddressParser readPointer uses correct size ──
|
||||
|
||||
void addressParserReadPointer32bit() {
|
||||
// Create a test provider with a 32-bit pointer at address 0
|
||||
TestProvider32 prov(4, 16);
|
||||
uint32_t val32 = 0xDEADBEEF;
|
||||
memcpy(prov.m_data.data(), &val32, 4);
|
||||
// Write garbage in bytes 4-7 to verify we only read 4 bytes
|
||||
memset(prov.m_data.data() + 4, 0xFF, 4);
|
||||
|
||||
AddressParserCallbacks cbs;
|
||||
int ptrSz = prov.pointerSize();
|
||||
auto* p = &prov;
|
||||
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = p->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
|
||||
auto result = AddressParser::evaluate("[0]", ptrSz, &cbs);
|
||||
QVERIFY(result.ok);
|
||||
QCOMPARE(result.value, (uint64_t)0xDEADBEEF);
|
||||
}
|
||||
|
||||
void addressParserReadPointer64bit() {
|
||||
TestProvider32 prov(8, 16);
|
||||
uint64_t val64 = 0x0000DEADBEEF1234ULL;
|
||||
memcpy(prov.m_data.data(), &val64, 8);
|
||||
|
||||
AddressParserCallbacks cbs;
|
||||
int ptrSz = prov.pointerSize();
|
||||
auto* p = &prov;
|
||||
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = p->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
|
||||
auto result = AddressParser::evaluate("[0]", ptrSz, &cbs);
|
||||
QVERIFY(result.ok);
|
||||
QCOMPARE(result.value, (uint64_t)0x0000DEADBEEF1234ULL);
|
||||
}
|
||||
|
||||
// ── 9. Source import HANDLE/LPVOID remain 64-bit by default ──
|
||||
|
||||
void sourceImportBackwardsCompat() {
|
||||
QString src = R"(
|
||||
struct Test {
|
||||
HANDLE h; // 0x0
|
||||
LPVOID lv; // 0x8
|
||||
};
|
||||
)";
|
||||
QString error;
|
||||
NodeTree tree = importFromSource(src, &error);
|
||||
QVERIFY(!tree.nodes.isEmpty());
|
||||
|
||||
// Default (no pointerSize arg) should be 64-bit
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.name == "h") QCOMPARE(n.kind, NodeKind::Pointer64);
|
||||
if (n.name == "lv") QCOMPARE(n.kind, NodeKind::Pointer64);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 10. Full round-trip: 32-bit import → generate → verify ──
|
||||
|
||||
void fullRoundTrip32bit() {
|
||||
QString src = R"(
|
||||
struct EPROCESS_32 {
|
||||
PVOID Pcb; // 0x0
|
||||
HANDLE UniqueProcessId; // 0x4
|
||||
DWORD ActiveProcessLinks; // 0x8
|
||||
};
|
||||
)";
|
||||
QString error;
|
||||
NodeTree tree = importFromSource(src, &error, 4);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||
QCOMPARE(tree.pointerSize, 4);
|
||||
|
||||
// Find the root struct
|
||||
uint64_t rootId = 0;
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||
rootId = n.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(rootId != 0);
|
||||
|
||||
// Generate C++ code
|
||||
QString code = renderCpp(tree, rootId);
|
||||
QVERIFY2(code.contains("void* Pcb;"),
|
||||
qPrintable("PVOID in 32-bit should generate void*:\n" + code));
|
||||
QVERIFY2(code.contains("void* UniqueProcessId;"),
|
||||
qPrintable("HANDLE in 32-bit should generate void*:\n" + code));
|
||||
|
||||
// Verify JSON persistence
|
||||
QJsonObject json = tree.toJson();
|
||||
QCOMPARE(json["pointerSize"].toInt(), 4);
|
||||
NodeTree restored = NodeTree::fromJson(json);
|
||||
QCOMPARE(restored.pointerSize, 4);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(Test32BitSupport)
|
||||
#include "test_32bit_support.moc"
|
||||
399
tests/test_addressparser.cpp
Normal file
399
tests/test_addressparser.cpp
Normal file
@@ -0,0 +1,399 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
// -- Identifier resolution --
|
||||
|
||||
void identBase() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "base");
|
||||
return *ok ? 0x140000000ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000000ULL);
|
||||
}
|
||||
|
||||
void identFieldName() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x140000000ULL; }
|
||||
if (name == "e_lfanew") { *ok = true; return 0xE8ULL; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base + e_lfanew", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x1400000E8ULL);
|
||||
}
|
||||
|
||||
void identUnknown() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString&, bool* ok) -> uint64_t {
|
||||
*ok = false; return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("unknown_var", 8, &cbs);
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("unknown identifier"));
|
||||
}
|
||||
|
||||
// -- Hex vs identifier disambiguation --
|
||||
|
||||
void hexDisambigDEAD() {
|
||||
// "DEAD" is all hex digits → should parse as hex number 0xDEAD
|
||||
auto r = AddressParser::evaluate("DEAD");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xDEADULL);
|
||||
}
|
||||
|
||||
void hexDisambigBase() {
|
||||
// "base" has 's' (non-hex) → identifier
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "base"); return *ok ? 42ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("base", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 42ULL);
|
||||
}
|
||||
|
||||
void hexDisambigABCwithUnderscore() {
|
||||
// "ABC_field" has '_' → identifier, not hex
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "ABC_field"); return *ok ? 99ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("ABC_field", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 99ULL);
|
||||
}
|
||||
|
||||
// -- Bitwise operators --
|
||||
|
||||
void bitwiseAnd() {
|
||||
auto r = AddressParser::evaluate("0xFF & 0x0F");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x0FULL);
|
||||
}
|
||||
|
||||
void bitwiseOr() {
|
||||
auto r = AddressParser::evaluate("0xA0 | 0x0B");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xABULL);
|
||||
}
|
||||
|
||||
void bitwiseXor() {
|
||||
auto r = AddressParser::evaluate("0xA ^ 0x5");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFULL);
|
||||
}
|
||||
|
||||
void shiftLeft() {
|
||||
auto r = AddressParser::evaluate("1 << 4");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x10ULL);
|
||||
}
|
||||
|
||||
void shiftRight() {
|
||||
auto r = AddressParser::evaluate("0xFF00 >> 8");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFFULL);
|
||||
}
|
||||
|
||||
// -- Unary bitwise NOT --
|
||||
|
||||
void unaryNot() {
|
||||
auto r = AddressParser::evaluate("~0");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFFFFFFFFFFFFFFFFULL);
|
||||
}
|
||||
|
||||
void unaryNotMask() {
|
||||
// ~0xFFF = 0xFFFFFFFFFFFFF000
|
||||
auto r = AddressParser::evaluate("~0xFFF");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xFFFFFFFFFFFFF000ULL);
|
||||
}
|
||||
|
||||
// -- Operator precedence --
|
||||
|
||||
void shiftPrecedence() {
|
||||
// C precedence: shift binds looser than addition
|
||||
// 1 + 2 << 3 = (1 + 2) << 3 = 3 << 3 = 24 = 0x18
|
||||
auto r = AddressParser::evaluate("1 + 2 << 3");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x18ULL);
|
||||
}
|
||||
|
||||
void andOrPrecedence() {
|
||||
// & binds tighter than |
|
||||
// 0xFF | 0x100 & 0xF00 = 0xFF | (0x100 & 0xF00) = 0xFF | 0x100 = 0x1FF
|
||||
auto r = AddressParser::evaluate("0xFF | 0x100 & 0xF00");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x1FFULL);
|
||||
}
|
||||
|
||||
void xorPrecedence() {
|
||||
// ^ between & and |: a | b ^ c & d = a | (b ^ (c & d))
|
||||
// 0xF0 | 0x0F ^ 0xFF & 0x0F = 0xF0 | (0x0F ^ (0xFF & 0x0F))
|
||||
// = 0xF0 | (0x0F ^ 0x0F) = 0xF0 | 0x00 = 0xF0
|
||||
auto r = AddressParser::evaluate("0xF0 | 0x0F ^ 0xFF & 0x0F");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xF0ULL);
|
||||
}
|
||||
|
||||
// -- E_lfanew end-to-end --
|
||||
|
||||
void elfanewScenario() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x140000000ULL; }
|
||||
if (name == "e_lfanew") { *ok = true; return 0xE8ULL; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
// base + e_lfanew = 0x140000000 + 0xE8 = 0x1400000E8
|
||||
auto r = AddressParser::evaluate("base + e_lfanew", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x1400000E8ULL);
|
||||
}
|
||||
|
||||
void pageAlignedExpr() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveIdentifier = [](const QString& name, bool* ok) -> uint64_t {
|
||||
if (name == "base") { *ok = true; return 0x140000000ULL; }
|
||||
if (name == "e_lfanew") { *ok = true; return 0xE8ULL; }
|
||||
*ok = false; return 0;
|
||||
};
|
||||
// (base + e_lfanew) & ~0xFFF = 0x1400000E8 & ~0xFFF = 0x140000000
|
||||
auto r = AddressParser::evaluate("(base + e_lfanew) & ~0xFFF", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000000ULL);
|
||||
}
|
||||
|
||||
// -- Validate with new syntax --
|
||||
|
||||
void validateIdentifier() {
|
||||
QCOMPARE(AddressParser::validate("base + e_lfanew"), QString());
|
||||
}
|
||||
|
||||
void validateBitwiseOps() {
|
||||
QCOMPARE(AddressParser::validate("0xFF & 0x0F"), QString());
|
||||
QCOMPARE(AddressParser::validate("1 << 4"), QString());
|
||||
QCOMPARE(AddressParser::validate("~0xFFF"), QString());
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -1922,7 +1924,7 @@ private slots:
|
||||
|
||||
void testCommandRowRootNameSpan() {
|
||||
// Name span should cover the class name in the merged command row
|
||||
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {";
|
||||
QString text = "source\u25BE 0x0 struct MyClass {";
|
||||
ColumnSpan nameSpan = commandRowRootNameSpan(text);
|
||||
QVERIFY(nameSpan.valid);
|
||||
|
||||
@@ -1984,6 +1986,647 @@ 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);
|
||||
}
|
||||
|
||||
// ── Static field node compose tests ──
|
||||
|
||||
void testStaticFieldHeaderLine() {
|
||||
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;
|
||||
|
||||
// Regular field
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "field_a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Static field node
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_static";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Header with "static" keyword and opening brace should appear
|
||||
QVERIFY2(result.text.contains(QStringLiteral("static "))
|
||||
&& result.text.contains(QStringLiteral("my_static"))
|
||||
&& result.text.contains(QStringLiteral("{")),
|
||||
qPrintable("Expected static field header in:\n" + result.text));
|
||||
}
|
||||
|
||||
void testStaticFieldDoesNotAffectStructSize() {
|
||||
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 f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Struct span without static field
|
||||
int spanBefore = tree.structSpan(rootId);
|
||||
|
||||
// Add static field
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Struct;
|
||||
sf.name = "static_field";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + 100");
|
||||
tree.addNode(sf);
|
||||
|
||||
int spanAfter = tree.structSpan(rootId);
|
||||
QCOMPARE(spanAfter, spanBefore);
|
||||
}
|
||||
|
||||
void testStaticFieldIsStaticLineFlag() {
|
||||
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 f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "field_a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_static";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// At least one line should have isStaticLine set
|
||||
bool foundStaticField = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.isStaticLine) {
|
||||
foundStaticField = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundStaticField, "Expected at least one LineMeta with isStaticLine=true");
|
||||
}
|
||||
|
||||
void testStaticFieldCollapsed() {
|
||||
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;
|
||||
|
||||
// Static field struct with a child (should still appear collapsed)
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Struct;
|
||||
sf.name = "inner";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
sf.collapsed = true;
|
||||
int hi = tree.addNode(sf);
|
||||
uint64_t sfId = tree.nodes[hi].id;
|
||||
|
||||
Node sfChild;
|
||||
sfChild.kind = NodeKind::UInt32;
|
||||
sfChild.name = "x";
|
||||
sfChild.parentId = sfId;
|
||||
sfChild.offset = 0;
|
||||
tree.addNode(sfChild);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// The static field's child should NOT have a visible line (it's collapsed)
|
||||
bool foundChildLine = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.nodeIdx >= 0 && lm.nodeIdx < tree.nodes.size()
|
||||
&& tree.nodes[lm.nodeIdx].name == QStringLiteral("x")
|
||||
&& tree.nodes[lm.nodeIdx].parentId == sfId) {
|
||||
foundChildLine = true;
|
||||
}
|
||||
}
|
||||
QVERIFY2(!foundChildLine,
|
||||
"Static field's children should not be visible when collapsed");
|
||||
}
|
||||
|
||||
void testStaticFieldExpressionShownInText() {
|
||||
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 sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_static";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + 0x10");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// The composed text should contain the expression and arrow
|
||||
QVERIFY2(result.text.contains(QStringLiteral("base + 0x10")),
|
||||
qPrintable("Expected expression in text:\n" + result.text));
|
||||
QVERIFY2(result.text.contains(QStringLiteral("\u2192")),
|
||||
qPrintable("Expected arrow (\u2192) in text:\n" + result.text));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCompose)
|
||||
|
||||
@@ -668,6 +668,243 @@ private slots:
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32);
|
||||
}
|
||||
// ── Static field node controller tests ──
|
||||
|
||||
void testAddStaticField() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int origSize = m_doc->tree.nodes.size();
|
||||
|
||||
// Simulate "Add Static Field" — same code as context menu action
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
const auto& h = m_doc->tree.nodes.back();
|
||||
QCOMPARE(h.isStatic, true);
|
||||
QCOMPARE(h.offsetExpr, QStringLiteral("base"));
|
||||
QCOMPARE(h.name, QStringLiteral("static_field"));
|
||||
QCOMPARE(h.parentId, rootId);
|
||||
}
|
||||
|
||||
void testAddStaticFieldUndo() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int origSize = m_doc->tree.nodes.size();
|
||||
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
|
||||
// Undo: static field should be gone
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize);
|
||||
|
||||
// Redo: static field should be back
|
||||
m_doc->undoStack.redo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
QCOMPARE(m_doc->tree.nodes.back().isStatic, true);
|
||||
}
|
||||
|
||||
void testChangeStaticFieldExpression() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Add a static field
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Change expression
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::ChangeOffsetExpr{sfId, QStringLiteral("base"), QStringLiteral("base + 0x10")}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + 0x10"));
|
||||
|
||||
// Undo: old expression restored
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
|
||||
}
|
||||
|
||||
void testDeleteStaticFieldPreservesStructSize() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int spanBefore = m_doc->tree.structSpan(rootId);
|
||||
|
||||
// Add a static field
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Struct size unchanged after adding static field
|
||||
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
|
||||
|
||||
// Remove static field
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{sfId}));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Struct size still unchanged
|
||||
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
|
||||
}
|
||||
|
||||
void testStaticFieldRenamePreservesExpression() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Add a static field
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("my_static");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + field_u32");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Rename the static field
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::Rename{sfId, QStringLiteral("my_static"), QStringLiteral("renamed_static")}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QStringLiteral("renamed_static"));
|
||||
// Expression should be preserved
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + field_u32"));
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
||||
}
|
||||
|
||||
// ── Test: clearing value history actually resets heat to 0 ──
|
||||
void testClearValueHistoryResetsHeat() {
|
||||
// Use a live provider so value tracking runs during refresh()
|
||||
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0);
|
||||
m_ctrl->setTrackValues(true);
|
||||
|
||||
// Do initial refresh to populate m_lastResult.meta
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find field_u32 nodeId
|
||||
uint64_t targetId = 0;
|
||||
for (const auto& n : m_doc->tree.nodes) {
|
||||
if (n.name == "field_u32") { targetId = n.id; break; }
|
||||
}
|
||||
QVERIFY(targetId != 0);
|
||||
|
||||
// Seed value history with multiple changes to get heat > 0
|
||||
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
|
||||
history[targetId].record("val_1");
|
||||
history[targetId].record("val_2");
|
||||
history[targetId].record("val_3");
|
||||
QVERIFY2(history[targetId].heatLevel() >= 2,
|
||||
"Pre-clear: should have heat >= 2 (warm)");
|
||||
|
||||
// Refresh so heatLevel propagates to LineMeta
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify heat is visible in meta
|
||||
bool foundHot = false;
|
||||
for (const auto& lm : m_ctrl->lastResult().meta) {
|
||||
if (lm.nodeId == targetId && lm.heatLevel > 0) {
|
||||
foundHot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundHot, "Pre-clear: LineMeta should show heat > 0");
|
||||
|
||||
// Now simulate what the "Clear Value History" context menu does:
|
||||
// remove from history map + clear subtree + refresh
|
||||
history.remove(targetId);
|
||||
for (int ci : m_doc->tree.subtreeIndices(targetId))
|
||||
history.remove(m_doc->tree.nodes[ci].id);
|
||||
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// After clear + refresh, heatLevel must be 0 for this node
|
||||
for (const auto& lm : m_ctrl->lastResult().meta) {
|
||||
if (lm.nodeId == targetId) {
|
||||
QCOMPARE(lm.heatLevel, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The history entry should exist again (re-recorded by refresh)
|
||||
// but with only 1 unique value → heatLevel 0
|
||||
QVERIFY(history.contains(targetId));
|
||||
QCOMPARE(history[targetId].heatLevel(), 0);
|
||||
QCOMPARE(history[targetId].uniqueCount(), 1);
|
||||
}
|
||||
|
||||
void testStaticFieldTypeChangePreservesFlags() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Change kind to UInt32
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::ChangeKind{sfId, NodeKind::Hex64, NodeKind::UInt32}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
|
||||
// Static field flags must survive type change
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestController)
|
||||
|
||||
@@ -671,6 +671,114 @@ private slots:
|
||||
QCOMPARE(h.count, 4); // 4 transitions
|
||||
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
|
||||
}
|
||||
|
||||
// ── Static field node serialization ──
|
||||
|
||||
void testStaticFieldJsonRoundTrip() {
|
||||
rcx::NodeTree tree;
|
||||
tree.baseAddress = 0x14000000;
|
||||
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "DOS_HEADER";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node field;
|
||||
field.kind = rcx::NodeKind::UInt32;
|
||||
field.name = "e_lfanew";
|
||||
field.parentId = rootId;
|
||||
field.offset = 0x3C;
|
||||
tree.addNode(field);
|
||||
|
||||
rcx::Node sf;
|
||||
sf.kind = rcx::NodeKind::Struct;
|
||||
sf.name = "nt_hdr";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + e_lfanew");
|
||||
tree.addNode(sf);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
|
||||
|
||||
QCOMPARE(tree2.nodes.size(), 3);
|
||||
const auto& h = tree2.nodes[2];
|
||||
QCOMPARE(h.isStatic, true);
|
||||
QCOMPARE(h.offsetExpr, QStringLiteral("base + e_lfanew"));
|
||||
QCOMPARE(h.name, QStringLiteral("nt_hdr"));
|
||||
}
|
||||
|
||||
void testStaticFieldJsonBackwardCompat() {
|
||||
// Old JSON without isStatic/offsetExpr should load with defaults
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
|
||||
|
||||
QCOMPARE(tree2.nodes[0].isStatic, false);
|
||||
QCOMPARE(tree2.nodes[0].offsetExpr, QString());
|
||||
}
|
||||
|
||||
void testStructSpanExcludesStaticFields() {
|
||||
using namespace rcx;
|
||||
NodeTree tree;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Regular field: offset 0, size 4
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Regular field: offset 4, size 8
|
||||
Node f2;
|
||||
f2.kind = NodeKind::UInt64;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 4;
|
||||
tree.addNode(f2);
|
||||
|
||||
// Static field: should NOT affect span
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Struct;
|
||||
sf.name = "static_field";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
// Span should be max(0+4, 4+8) = 12, same as without static field
|
||||
QCOMPARE(tree.structSpan(rootId), 12);
|
||||
}
|
||||
|
||||
void testStaticExprSpanFor() {
|
||||
using namespace rcx;
|
||||
// Simulate a static field body line: " return base + e_lfanew → 0x1400000E8"
|
||||
LineMeta lm;
|
||||
lm.isStaticLine = true;
|
||||
QString lineText = QStringLiteral(" return base + e_lfanew \u2192 0x1400000E8");
|
||||
ColumnSpan span = staticExprSpanFor(lm, lineText);
|
||||
QVERIFY(span.valid);
|
||||
QString expr = lineText.mid(span.start, span.end - span.start);
|
||||
QCOMPARE(expr.trimmed(), QStringLiteral("base + e_lfanew"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCore)
|
||||
|
||||
@@ -4,62 +4,92 @@
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
|
||||
int main()
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
const char* connStr = "tcp:Port=5057,Server=localhost";
|
||||
const char* connStr = "tcp:Port=5055,Server=localhost";
|
||||
if (argc > 1) connStr = argv[1];
|
||||
|
||||
// Initialize COM — required for DbgEng remote transport (TCP/named-pipe)
|
||||
HRESULT hrCom = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||
printf("CoInitializeEx: 0x%08lX\n", hrCom);
|
||||
fflush(stdout);
|
||||
|
||||
printf("Attempting DebugConnect(\"%s\")...\n", connStr);
|
||||
fflush(stdout);
|
||||
|
||||
IDebugClient* client = nullptr;
|
||||
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
|
||||
printf("DebugConnect returned: 0x%08lX\n", hr);
|
||||
fflush(stdout);
|
||||
|
||||
if (SUCCEEDED(hr) && client) {
|
||||
printf("Connected! Getting IDebugDataSpaces...\n");
|
||||
printf("Connected! Getting interfaces...\n");
|
||||
fflush(stdout);
|
||||
|
||||
IDebugDataSpaces* ds = nullptr;
|
||||
hr = client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
|
||||
printf("QueryInterface(IDebugDataSpaces) = 0x%08lX\n", hr);
|
||||
fflush(stdout);
|
||||
|
||||
if (ds) {
|
||||
IDebugControl* ctrl = nullptr;
|
||||
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||
IDebugControl* ctrl = nullptr;
|
||||
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||
|
||||
if (ctrl) {
|
||||
printf("Waiting for event...\n");
|
||||
hr = ctrl->WaitForEvent(0, 5000);
|
||||
printf("WaitForEvent = 0x%08lX\n", hr);
|
||||
ctrl->Release();
|
||||
}
|
||||
if (ctrl) {
|
||||
printf("Calling WaitForEvent(5000ms)...\n");
|
||||
fflush(stdout);
|
||||
hr = ctrl->WaitForEvent(0, 5000);
|
||||
printf("WaitForEvent = 0x%08lX\n", hr);
|
||||
fflush(stdout);
|
||||
|
||||
// Try to read 2 bytes
|
||||
IDebugSymbols* sym = nullptr;
|
||||
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||
if (sym) {
|
||||
ULONG numMods = 0, numUnloaded = 0;
|
||||
hr = sym->GetNumberModules(&numMods, &numUnloaded);
|
||||
printf("GetNumberModules = 0x%08lX, numMods=%lu\n", hr, numMods);
|
||||
|
||||
if (numMods > 0) {
|
||||
ULONG64 base = 0;
|
||||
hr = sym->GetModuleByIndex(0, &base);
|
||||
printf("Module[0] base = 0x%llX (hr=0x%08lX)\n", base, hr);
|
||||
|
||||
if (SUCCEEDED(hr) && base) {
|
||||
uint8_t buf[4] = {};
|
||||
ULONG got = 0;
|
||||
hr = ds->ReadVirtual(base, buf, 4, &got);
|
||||
printf("ReadVirtual(%llX, 4) = 0x%08lX, got=%lu, data=[%02X %02X %02X %02X]\n",
|
||||
base, hr, got, buf[0], buf[1], buf[2], buf[3]);
|
||||
}
|
||||
}
|
||||
sym->Release();
|
||||
}
|
||||
ds->Release();
|
||||
ULONG debugClass = 0, debugQual = 0;
|
||||
hr = ctrl->GetDebuggeeType(&debugClass, &debugQual);
|
||||
printf("GetDebuggeeType = 0x%08lX, class=%lu, qualifier=%lu\n",
|
||||
hr, debugClass, debugQual);
|
||||
printf(" -> %s\n", debugQual >= 1024 ? "DUMP" : "LIVE");
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
IDebugSymbols* sym = nullptr;
|
||||
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||
|
||||
if (sym) {
|
||||
ULONG numMods = 0, numUnloaded = 0;
|
||||
hr = sym->GetNumberModules(&numMods, &numUnloaded);
|
||||
printf("GetNumberModules = 0x%08lX, loaded=%lu, unloaded=%lu\n",
|
||||
hr, numMods, numUnloaded);
|
||||
fflush(stdout);
|
||||
|
||||
if (numMods > 0) {
|
||||
ULONG64 base = 0;
|
||||
hr = sym->GetModuleByIndex(0, &base);
|
||||
printf("Module[0] base = 0x%llX (hr=0x%08lX)\n", base, hr);
|
||||
fflush(stdout);
|
||||
|
||||
if (SUCCEEDED(hr) && base && ds) {
|
||||
uint8_t buf[4] = {};
|
||||
ULONG got = 0;
|
||||
hr = ds->ReadVirtual(base, buf, 4, &got);
|
||||
printf("ReadVirtual(0x%llX, 4) = 0x%08lX, got=%lu, data=[%02X %02X %02X %02X]\n",
|
||||
base, hr, got, buf[0], buf[1], buf[2], buf[3]);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
sym->Release();
|
||||
}
|
||||
|
||||
if (ds) ds->Release();
|
||||
if (ctrl) ctrl->Release();
|
||||
|
||||
printf("Disconnecting...\n");
|
||||
fflush(stdout);
|
||||
client->EndSession(DEBUG_END_DISCONNECT);
|
||||
client->Release();
|
||||
printf("Done.\n");
|
||||
} else {
|
||||
printf("DebugConnect FAILED. hr=0x%08lX\n", hr);
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
if (SUCCEEDED(hrCom)) CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user